@kazurolabs/psql-explorer 0.0.0-stage → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +505 -2
- package/RELEASES.md +87 -0
- package/dist/app.js +39 -0
- package/dist/cli.js +307 -0
- package/dist/commands/configgen.js +128 -0
- package/dist/commands/explore.js +388 -0
- package/dist/commands/terminal-prompt.js +68 -0
- package/dist/config/load.js +324 -0
- package/dist/config/resolve-environment.js +112 -0
- package/dist/config/resolvers/env.js +12 -0
- package/dist/config/resolvers/file.js +24 -0
- package/dist/config/resolvers/groups.js +1 -0
- package/dist/config/resolvers/index.js +49 -0
- package/dist/config/resolvers/literal.js +7 -0
- package/dist/config/resolvers/prompt.js +159 -0
- package/dist/config/resolvers/stored.js +13 -0
- package/dist/config/resolvers/types.js +18 -0
- package/dist/config/resolvers/vault.js +283 -0
- package/dist/config/schema.js +24 -0
- package/dist/config/source.js +105 -0
- package/dist/db/connect.js +55 -0
- package/dist/db/identifiers.js +6 -0
- package/dist/db/paths.js +14 -0
- package/dist/db/store.js +323 -0
- package/dist/db/tester.js +190 -0
- package/dist/db/validate.js +99 -0
- package/dist/environments.js +140 -0
- package/dist/explorer/alter.js +493 -0
- package/dist/explorer/catalog.js +414 -0
- package/dist/explorer/connect-entry.js +42 -0
- package/dist/explorer/cypher-parse.js +478 -0
- package/dist/explorer/cypher.js +963 -0
- package/dist/explorer/ddl.js +502 -0
- package/dist/explorer/dependents.js +222 -0
- package/dist/explorer/design-load.js +254 -0
- package/dist/explorer/design.js +45 -0
- package/dist/explorer/edit.js +108 -0
- package/dist/explorer/explain.js +37 -0
- package/dist/explorer/filter.js +121 -0
- package/dist/explorer/format.js +134 -0
- package/dist/explorer/graph.js +679 -0
- package/dist/explorer/gucs.js +31 -0
- package/dist/explorer/profile.js +102 -0
- package/dist/explorer/references.js +95 -0
- package/dist/explorer/rows.js +64 -0
- package/dist/explorer/schema-diff.js +360 -0
- package/dist/explorer/search.js +309 -0
- package/dist/explorer/security.js +152 -0
- package/dist/explorer/seed.js +1376 -0
- package/dist/explorer/session.js +122 -0
- package/dist/explorer/stats.js +135 -0
- package/dist/explorer/timeout.js +13 -0
- package/dist/prompts.js +52 -0
- package/dist/screens/environments.js +229 -0
- package/dist/screens/explorer.js +145 -0
- package/dist/screens/header.js +8 -0
- package/dist/screens/index.js +13 -0
- package/dist/screens/new-connection.js +178 -0
- package/dist/screens/panes/common.js +35 -0
- package/dist/screens/panes/history.js +36 -0
- package/dist/screens/panes/info.js +61 -0
- package/dist/screens/panes/query.js +113 -0
- package/dist/screens/panes/table.js +178 -0
- package/dist/screens/panes/tables.js +142 -0
- package/dist/screens/prompt-modal.js +18 -0
- package/dist/screens/record-view.js +42 -0
- package/dist/screens/result-grid.js +126 -0
- package/dist/screens/scroll.js +12 -0
- package/dist/screens/splash.js +78 -0
- package/dist/screens/terminal.js +25 -0
- package/dist/screens/types.js +1 -0
- package/dist/services.js +1 -0
- package/dist/startup.js +84 -0
- package/dist/web/server.js +988 -0
- package/package.json +69 -5
- package/web/dist/assets/anton-latin-400-normal-AUNGEG_V.woff +0 -0
- package/web/dist/assets/anton-latin-400-normal-Byf51wtH.woff2 +0 -0
- package/web/dist/assets/anton-latin-ext-400-normal-BMODBQc6.woff +0 -0
- package/web/dist/assets/anton-latin-ext-400-normal-SyiqE2Jt.woff2 +0 -0
- package/web/dist/assets/anton-vietnamese-400-normal-2FfR1wHA.woff +0 -0
- package/web/dist/assets/anton-vietnamese-400-normal-CkBxLiRJ.woff2 +0 -0
- package/web/dist/assets/antonio-latin-400-normal-B7iMKlzw.woff +0 -0
- package/web/dist/assets/antonio-latin-400-normal-CZjzwNAN.woff2 +0 -0
- package/web/dist/assets/antonio-latin-700-normal-BY3mCh47.woff +0 -0
- package/web/dist/assets/antonio-latin-700-normal-DSH-lEDG.woff2 +0 -0
- package/web/dist/assets/antonio-latin-ext-400-normal-nE82dizb.woff2 +0 -0
- package/web/dist/assets/antonio-latin-ext-400-normal-qA9Jb92z.woff +0 -0
- package/web/dist/assets/antonio-latin-ext-700-normal-BdSkr58W.woff +0 -0
- package/web/dist/assets/antonio-latin-ext-700-normal-D-dvgqjr.woff2 +0 -0
- package/web/dist/assets/button-ClA70PLR.js +1 -0
- package/web/dist/assets/code-xml-CXkpHeU9.js +1 -0
- package/web/dist/assets/compare-view-DKl5l_Jw.js +2 -0
- package/web/dist/assets/copy-CCXzeW-s.js +1 -0
- package/web/dist/assets/cypher-view-Pd0WFH8F.js +13 -0
- package/web/dist/assets/design-view-CIHSmH4u.js +9 -0
- package/web/dist/assets/erd-view-CV7mFGAN.js +1 -0
- package/web/dist/assets/format-sql-BLj5_RDZ.js +5 -0
- package/web/dist/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
- package/web/dist/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
- package/web/dist/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
- package/web/dist/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
- package/web/dist/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
- package/web/dist/assets/graph-force-yBLyErpS.js +1 -0
- package/web/dist/assets/graph-link-OCo_s0zO.js +1 -0
- package/web/dist/assets/graph-view-sM1OlByf.js +3 -0
- package/web/dist/assets/index-CDcnUacl.js +10 -0
- package/web/dist/assets/index-OzJqnJWG.css +2 -0
- package/web/dist/assets/key-round-Cv_7QeED.js +1 -0
- package/web/dist/assets/loader-circle-BK_GMdvN.js +1 -0
- package/web/dist/assets/orbitron-latin-700-normal-4jsRXGGJ.woff2 +0 -0
- package/web/dist/assets/orbitron-latin-700-normal-DmYOh-W9.woff +0 -0
- package/web/dist/assets/orbitron-latin-900-normal-BbJatbhp.woff +0 -0
- package/web/dist/assets/orbitron-latin-900-normal-DrIi7unX.woff2 +0 -0
- package/web/dist/assets/query-builder-view-ChNFb51g.js +4 -0
- package/web/dist/assets/rotate-ccw-CdCaf7Q6.js +1 -0
- package/web/dist/assets/route-DO6IZ7of.js +15 -0
- package/web/dist/assets/seed-view-DFNB8WHE.js +7 -0
- package/web/dist/assets/share-tech-mono-latin-400-normal-15MgdvEM.woff +0 -0
- package/web/dist/assets/share-tech-mono-latin-400-normal-CCqdbj5z.woff2 +0 -0
- package/web/dist/assets/shield-alert-CMS-3j4U.js +1 -0
- package/web/dist/assets/sliders-horizontal-Dg51yc-C.js +1 -0
- package/web/dist/assets/sql-tabs-B4jt6bQj.js +2 -0
- package/web/dist/assets/table-DfMz2f8M.js +1 -0
- package/web/dist/assets/triangle-alert-COGnHCD1.js +1 -0
- package/web/dist/assets/zone-D9dXk8tn.js +5 -0
- package/web/dist/index.html +26 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kazuro Labs
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,506 @@
|
|
|
1
|
-
#
|
|
1
|
+
# psql-explorer
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Explore your Postgres databases from the terminal: browse schemas and tables, page through rows,
|
|
4
|
+
look at columns, indexes and constraints, and run SQL, in an interactive TUI or from the command
|
|
5
|
+
line. It is in the spirit of pgweb, without a browser.
|
|
6
|
+
|
|
7
|
+
A team shares one set of environments without a database of its own: the config is a folder of
|
|
8
|
+
small YAML files, kept in a directory or in HashiCorp Vault, and every password is looked up when
|
|
9
|
+
it is needed (from Vault, an environment variable, a file, a prompt, ...). Production environments
|
|
10
|
+
are opened **read only** by default, so nothing typed there can change data.
|
|
11
|
+
|
|
12
|
+
> **Disclaimer:** psql-explorer is under active development and has not reached a stable release.
|
|
13
|
+
> It connects to real databases, including production ones, and runs the SQL you give it. Check
|
|
14
|
+
> which environment you are in, keep production read only, and expect breaking changes between
|
|
15
|
+
> versions. It is provided as is, without warranty (see [LICENSE](LICENSE)).
|
|
16
|
+
|
|
17
|
+
## Requirements
|
|
18
|
+
|
|
19
|
+
- Node.js 22.13 or newer
|
|
20
|
+
- PostgreSQL databases
|
|
21
|
+
- An interactive terminal for the TUI. The commands (`tables`, `query`, ...) need none.
|
|
22
|
+
|
|
23
|
+
## Install and run
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
npx @kazurolabs/psql-explorer # once, without installing
|
|
27
|
+
npm i -g @kazurolabs/psql-explorer # or install the psql-explorer command
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Quick start
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
export PSQL_EXPLORER_ENVIRONMENTS=vault:secret/data/psql-explorer # or a directory
|
|
34
|
+
export VAULT_ADDR=https://vault.example.com:8200
|
|
35
|
+
vault login # or VAULT_TOKEN, see below
|
|
36
|
+
psql-explorer
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Pick an environment and press `Enter`. Without a shared config psql-explorer still starts, and `n`
|
|
40
|
+
saves your own connections.
|
|
41
|
+
|
|
42
|
+
## Web UI
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
psql-explorer --web [--port 7878] [--no-open] # serves the UI and API on http://127.0.0.1:7878 and opens it
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The UI lives in `web/` (Vite, React, Tailwind, shadcn/ui) and is powered by the same config,
|
|
49
|
+
Vault lookups and read-only rules as the TUI. A value the config says to `prompt` for (a password, say) is asked for in a dialog in the page, and remembered until the server stops. Picking an environment opens its database: the schemas on the left open to
|
|
50
|
+
their tables, views, functions, sequences and types, and the one you pick is shown on the right
|
|
51
|
+
(columns, indexes, constraints, definition and rows for a table; the source of a function; and so on).
|
|
52
|
+
Every view has an address of its own, e.g. `#/staging/table/app/users`. The server listens on loopback only. From a checkout, `npm run dev:web` starts the API and Vite with hot reload together (run
|
|
53
|
+
`npm run install:web` once first), and `npm run build` compiles the CLI and builds `web/dist`.
|
|
54
|
+
|
|
55
|
+
### Settings on the SQL page
|
|
56
|
+
|
|
57
|
+
The **Settings** button on the SQL page sets Postgres settings (GUCs) such as `statement_timeout`,
|
|
58
|
+
`work_mem`, `search_path` or a custom `app.tenant_id` on the session before each Run, Explain and
|
|
59
|
+
export. They are kept per environment in the browser. In a read only environment the read only
|
|
60
|
+
settings themselves cannot be changed.
|
|
61
|
+
|
|
62
|
+
### Seed page
|
|
63
|
+
|
|
64
|
+
Beside **SQL**, a **Seed** tab fills a table with generated rows, for building up a development
|
|
65
|
+
database (`#/dev/seed/app/users`). Pick a table, then a generator for each column: Faker's people,
|
|
66
|
+
places, companies, internet, text, numbers and dates, plus a fixed value, one of a list, a counter, a
|
|
67
|
+
Faker template such as `{{person.firstName}}-{{number.int(99)}}`, or an existing row of the table
|
|
68
|
+
a foreign key points at. Each column starts on a suggestion from its name, type and constraints
|
|
69
|
+
(serials, identities and `now()` defaults keep their default; enums and `CHECK (x IN (...))` lists
|
|
70
|
+
become "one of a list", where each allowed value can be ticked or not and given a weight, so
|
|
71
|
+
`shipped` can come up six times as often as `paid`; a typed list takes weights one a line), and has
|
|
72
|
+
its own options, a share of NULLs and a "unique" switch. Choose
|
|
73
|
+
how many rows (up to 100,000), a locale and optionally a seed (the same seed makes the same rows),
|
|
74
|
+
preview the first ten, then insert. All rows go in one transaction, so a run adds every row or
|
|
75
|
+
none; "Skip rows that clash" adds `ON CONFLICT DO NOTHING` instead. A unique column tries 100 times
|
|
76
|
+
for a value it has not made yet and then fails the run; "Skip rows that run out of unique values"
|
|
77
|
+
leaves that row out instead. Either skip can add a few fewer rows than asked for, and the result
|
|
78
|
+
says how many were skipped. Your picks for a table are kept in the browser.
|
|
79
|
+
|
|
80
|
+
Columns can depend on each other, so rows hang together. **Add condition** under a column's
|
|
81
|
+
generator adds "when ‹another column› is / is not / is one of / > / < / is NULL ‹value›, use ‹this
|
|
82
|
+
generator›" cases: for a book, `num_pages` can be 10 to 1000 when `is_digital` is true and 100 to
|
|
83
|
+
2000 otherwise, and `file_mb` NULL unless it is digital. The first case that matches is used, and
|
|
84
|
+
the column's own generator otherwise. A template can also use another column of the same row,
|
|
85
|
+
e.g. `{{row.first_name}}.{{row.last_name}}@{{internet.domainName}}` for an email that matches the
|
|
86
|
+
name. Columns are made in the order this needs, so conditions may point either way; a loop, or a
|
|
87
|
+
condition on a column left on its default (whose value is not known), is refused.
|
|
88
|
+
|
|
89
|
+
Rows can take values from other tables through **Linked tables**. Each foreign key of the table is
|
|
90
|
+
a link already (named after its constraint), and **Link a table** adds any other table, say a
|
|
91
|
+
lookup table. A link gives each new row one row of its table, and every column that reads the link
|
|
92
|
+
reads that same row, so all the columns of a composite foreign key always point at a row that
|
|
93
|
+
exists. Columns read a link with "Existing row it references" (a foreign key column),
|
|
94
|
+
**Column of a linked table** (any link and column), or `{{link.name.column}}` in a template. A
|
|
95
|
+
link either picks **a random row for each new row** (optionally each row once, for a one-to-one
|
|
96
|
+
key) or makes **new rows for each of its rows**: set the orders link of `order_lines` that way with
|
|
97
|
+
3 to 5 rows for each, and every order gets 3 to 5 lines; the number of rows to insert then follows
|
|
98
|
+
from the table. Several such links make rows for every combination of their rows. **Counting up for
|
|
99
|
+
each linked row** starts again for each of them, for line numbers 1, 2, 3 of each order.
|
|
100
|
+
|
|
101
|
+
Array columns (`text[]`, `int[]`, an array of an enum) get a list per row: **Some of a list** picks
|
|
102
|
+
between a min and max of distinct values from the ones you give (an enum array starts with its
|
|
103
|
+
labels), and any other generator is repeated for each item, with a min and max number of items
|
|
104
|
+
per array (1 to 3 unless you say). A fixed value written as an array literal, like `{a,b}`, is used
|
|
105
|
+
as it is.
|
|
106
|
+
|
|
107
|
+
The tab is only offered where the environment says `seeder: true`, which is the default for the
|
|
108
|
+
`development` and `local` tiers (and for local connections). A read only environment can only
|
|
109
|
+
preview, unless it is protected and switched to write mode.
|
|
110
|
+
|
|
111
|
+
### Graph page
|
|
112
|
+
|
|
113
|
+
A **Graph** tab beside **Compare** draws rows and the rows linked to them through foreign keys, so
|
|
114
|
+
"this order, its customer, its lines and their products" can be seen at once
|
|
115
|
+
(`#/dev/graph/app/orders?filters=...&depth=1`). It only reads, and works on every environment. Pick a
|
|
116
|
+
table, narrow it with the same filters as the Rows tab, choose a depth (0 to 3) and a direction
|
|
117
|
+
(rows it points at, rows pointing at it, or both), and press **Draw**. Each row is a dot coloured by its
|
|
118
|
+
table that pushes the others apart while its links pull it towards the rows it is linked to (a
|
|
119
|
+
force-directed layout). The dot is named by a label from the row's data, and under it, fainter, is the
|
|
120
|
+
name of its table. An arrow runs from the row holding a key to the row it points at (hover for the
|
|
121
|
+
key's name). Click a dot to follow its links, shift-click to see its row, and click a "+37 more in
|
|
122
|
+
order_lines" node to load the next rows of a key. Drag a dot to move it, drag the background to move
|
|
123
|
+
around, scroll to zoom, and **Re-layout** lays it out afresh and fits it to the screen. The legend hides
|
|
124
|
+
and shows tables and keys. (The earlier layout, in columns of cards by distance, is still in the code
|
|
125
|
+
as `Canvas` in `web/src/components/graph-view.tsx`, but the page no longer uses it.)
|
|
126
|
+
|
|
127
|
+
A **schema map** above the graph is a small entity relationship diagram of the table's schema: a box
|
|
128
|
+
for each table (with the number of its rows drawn) and an arrow for each foreign key. Click a table to
|
|
129
|
+
leave it out of the graph, which then goes through it as it does for a join table, and click an arrow to
|
|
130
|
+
stop following that key; a bar offers **Draw again** (or **Undo**) to apply the changes. Clusters of
|
|
131
|
+
rows that are not linked to each other push each other further apart than rows of one cluster do.
|
|
132
|
+
|
|
133
|
+
**Labels and tables** sets how the drawing is named, for each kind of row in it. The first time a table
|
|
134
|
+
shows up it is asked about: a row is named by its primary key (the values joined for a composite key)
|
|
135
|
+
until other fields are picked, and several picked fields are joined too. A table can also be
|
|
136
|
+
**removed from the graph**, which is for join tables of many to many relationships: its rows are not
|
|
137
|
+
drawn, and the rows at either end are linked directly by a dashed line (named `via` the table) as if it
|
|
138
|
+
were not there. This also works for a chain: with `orders` and `order_lines` both removed, a customer is
|
|
139
|
+
linked to the products it ordered (`via orders › order_lines`). The foreign keys are followed in either
|
|
140
|
+
direction through up to three removed tables, with one join that gives the distinct pairs of rows, and
|
|
141
|
+
the shortest way to each table is used. A table that a key leads to directly is not also reached the long
|
|
142
|
+
way. At most 20 rows are linked to a row this way, and the graph says when that cut some off. The tables that look like join tables (two or more keys leaving their rows) are marked.
|
|
143
|
+
Removing or restoring a table draws the graph again. These choices, and the keys not followed, are remembered in the browser for each
|
|
144
|
+
environment.
|
|
145
|
+
|
|
146
|
+
The Rows tab has a **Graph** link that starts from its filters, and the row drawer a graph button that
|
|
147
|
+
starts from that row.
|
|
148
|
+
In the **SQL** tab, a result whose columns include a table's primary key gets a checkbox on each row;
|
|
149
|
+
tick some, pick which table they are rows of (the ones the statement names come first) and follow
|
|
150
|
+
**Graph N rows** to draw exactly those rows (`?keys=...` in the address). The address carries the table, filters, depth, direction and the cards that were
|
|
151
|
+
opened, so a copied link draws the same graph. **Export** saves the drawing as SVG or PNG, or the nodes
|
|
152
|
+
(with their rows) and edges as JSON.
|
|
153
|
+
|
|
154
|
+
### Cypher page
|
|
155
|
+
|
|
156
|
+
A **Cypher** tab queries the database in a small, read only subset of Cypher, the language of Neo4j. A
|
|
157
|
+
label is a table and a relationship is a foreign key, so you ask for the shape of what you want rather
|
|
158
|
+
than writing the joins. To find products that customers buy together:
|
|
159
|
+
|
|
160
|
+
```cypher
|
|
161
|
+
MATCH (a:Product)<-[]-(c:Customer)-[]->(b:Product)
|
|
162
|
+
WHERE a.id < b.id
|
|
163
|
+
RETURN a, b, count(DISTINCT c) AS customers
|
|
164
|
+
ORDER BY customers DESC
|
|
165
|
+
LIMIT 25
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Rows that are returned (`a`, `b`) are drawn as a graph, linked as the pattern links them, and a number the
|
|
169
|
+
query returns for each link (`customers`) labels it and sets its thickness. There is also a table of the
|
|
170
|
+
result. **Show SQL** shows the statement the query becomes, with the values written in, and how each
|
|
171
|
+
relationship was joined. The query is also in the address (`#/dev/cypher?q=...&run=1`).
|
|
172
|
+
|
|
173
|
+
- **Labels** name a table: `Customer`, `customers` and `OrderLine` (for `order_lines`) all work, and a
|
|
174
|
+
name in backticks may carry a schema, as in `` `shop.customers` ``. The environment's schema is tried first;
|
|
175
|
+
a name found in two other schemas must be given with one.
|
|
176
|
+
- **Relationships** follow a foreign key: `-[]->` from the table holding it, `<-[]-` to it, `-[]-` either
|
|
177
|
+
way. Tables with no key between them are joined through the shortest chain of keys through other tables
|
|
178
|
+
(up to three), as customers to products are through orders and order lines; the direction of the arrow
|
|
179
|
+
is not used for such a chain. When two keys, or two chains, are equally good, the error lists them and
|
|
180
|
+
a hint in the brackets picks one: the name of a key or its column (`-[:billing_address_id]-`), or a
|
|
181
|
+
table on the way (`-[:orders]-`). The nodes along a chain can also be written out.
|
|
182
|
+
A relationship cannot be named, and has no properties; paths of any length (`*`) are not supported.
|
|
183
|
+
- **Properties** in a node (`(c:Customer {country: 'NZ'})`) and **WHERE** narrow the rows: `=`, `<>`, `<`,
|
|
184
|
+
`<=`, `>`, `>=`, `IN [...]`, `IS [NOT] NULL`, `STARTS WITH`, `ENDS WITH`, `CONTAINS`, `=~` (a regular
|
|
185
|
+
expression for the whole value), `AND`, `OR`, `NOT`, and `toLower`, `toUpper`, `coalesce`, `abs`. Two
|
|
186
|
+
rows are compared with `=` and `<>` by their primary keys, which is how to leave out a row paired with
|
|
187
|
+
itself (`a <> b`), or to count each pair once (`a.id < b.id`).
|
|
188
|
+
- **RETURN** takes rows, columns (`c.name`, optionally `AS`) and `count`, `sum`, `avg`, `min`, `max` and
|
|
189
|
+
`collect`, which group by whatever else is returned; with `DISTINCT`, `ORDER BY`, `SKIP` and `LIMIT`.
|
|
190
|
+
`RETURN *` returns every named node.
|
|
191
|
+
- Two relationships that follow the same keys use different rows, as in Cypher (so a row is never its own
|
|
192
|
+
neighbour); the same row may still be matched by two nodes, which `<>` rules out.
|
|
193
|
+
- Not supported: anything that writes, `OPTIONAL MATCH`, `WITH`, `UNWIND`, `UNION`, parameters and
|
|
194
|
+
arithmetic.
|
|
195
|
+
|
|
196
|
+
Before a query runs, the SQL it becomes is **planned** (`EXPLAIN`, which runs nothing) and assessed. A
|
|
197
|
+
query that looks heavy shows what makes it so: a pattern in parts that nothing joins (a cross join), many
|
|
198
|
+
tables, grouping or sorting large tables that nothing narrows, or a plan that Postgres estimates at a high
|
|
199
|
+
cost or with a join producing millions of rows. A medium warning is shown with the result. A high one is
|
|
200
|
+
not run until **Run anyway** is pressed. A query that runs does so in a read only transaction that is
|
|
201
|
+
stopped after 15 seconds, and reads at most 200 rows (1000 with a `LIMIT`).
|
|
202
|
+
|
|
203
|
+
### Build page
|
|
204
|
+
|
|
205
|
+
A **Build** tab makes a `SELECT` from a form, for when you would rather click than type
|
|
206
|
+
(`#/dev/build`). Pick a table to start from, then add what you need and the SQL is written beside it as
|
|
207
|
+
you go:
|
|
208
|
+
|
|
209
|
+
- **Join**: the tables related by a foreign key are offered first, with the columns that match already
|
|
210
|
+
filled in. Any other table can be joined too, on any two columns you pick: a foreign key is not
|
|
211
|
+
needed. A join can be inner, left, right, full or cross, take several tests (`=`, `<>`, `<`, ...), and
|
|
212
|
+
compare as text when the types differ. Each test says whether it follows a foreign key, and columns of
|
|
213
|
+
different types are flagged before Postgres refuses them.
|
|
214
|
+
- **Columns**: any column of any table, a count of rows, or a count, count distinct, sum, average, min
|
|
215
|
+
or max; a date can be cut into days, months, ... in a time zone. Columns that are not totals are
|
|
216
|
+
grouped by.
|
|
217
|
+
- **Filter**: tests that fit the column's type (contains, starts with, one of a list, between, within
|
|
218
|
+
the last 7 days, is empty, ...), against a value or another column, in groups of "all of" or "any
|
|
219
|
+
of" inside each other. A filter that is not filled in yet is left out. **Having** filters the totals.
|
|
220
|
+
- **Sort and limit**, and unique rows only.
|
|
221
|
+
|
|
222
|
+
**Run** shows the result with pages and export, **Edit as SQL** opens the statement in the SQL page, and
|
|
223
|
+
the address carries the whole form, so a link opens the same query. The form is also kept in the
|
|
224
|
+
browser per environment. The page only ever writes a `SELECT` (values are quoted, names are quoted when
|
|
225
|
+
they need it, and a statement is checked to read before it runs), so it works the same in a read only
|
|
226
|
+
environment. Joins, filters, grouping, sorting and a limit are covered; subqueries, `WITH` and window
|
|
227
|
+
functions are for the SQL page.
|
|
228
|
+
|
|
229
|
+
### Design page
|
|
230
|
+
|
|
231
|
+
A **Design** tab makes tables from a form and changes the ones that exist, always showing the SQL it
|
|
232
|
+
will run (`#/dev/design/new`, and `#/dev/design/app/orders` to change `app.orders`; the table view
|
|
233
|
+
has an **Edit design** button). A form holds the table (schema, name, comment, unlogged, partitioning
|
|
234
|
+
by range, list or hash), a grid of columns (add, remove, reorder; built-in types with their sizes,
|
|
235
|
+
the database's enums, domains and composite types, arrays, `NOT NULL`, a default, identity, a stored
|
|
236
|
+
generated expression and a comment), constraints (primary key, unique, check, foreign key with its
|
|
237
|
+
actions, `MATCH FULL` and `DEFERRABLE`) and indexes (columns or expressions, unique, btree, hash, gin,
|
|
238
|
+
gist or brin, a partial `WHERE`, `INCLUDE`). Problems show as you type: a duplicate name, a foreign key
|
|
239
|
+
whose column types differ, a primary key on a nullable column (it becomes `NOT NULL`). The SQL is
|
|
240
|
+
formatted beside the form, with **Copy** and **Open in SQL**. **Create** runs it in one transaction and
|
|
241
|
+
names the statement that failed; afterwards **Seed it** opens the Seed page on the new table where
|
|
242
|
+
the environment allows seeding. A new form is kept in the browser until the table is created or cleared.
|
|
243
|
+
|
|
244
|
+
Editing loads the table into the same form and works out the `ALTER` statements from what you change:
|
|
245
|
+
adding, dropping and renaming columns, constraints and indexes (rows are followed by their own id, so
|
|
246
|
+
a rename is a `RENAME` and not a drop and an add), a changed type with a `USING` expression you can
|
|
247
|
+
write, defaults, `NOT NULL`, identity, comments, and renaming the table or moving it to another schema.
|
|
248
|
+
Partitioning, exclusion constraints and indexes with a custom operator class or ordering cannot be
|
|
249
|
+
changed here, and the page says which parts it leaves alone. Next to a statement are the warnings
|
|
250
|
+
that apply, with the table's row estimate: a type change that rewrites the table, `NOT NULL` or a new
|
|
251
|
+
constraint that reads every row. A foreign key or check can be added `NOT VALID` and then validated in a
|
|
252
|
+
second statement, and an index can be built `CONCURRENTLY`, which cannot run in a transaction and so
|
|
253
|
+
runs afterwards. Everything else runs in one transaction that starts with `SET LOCAL lock_timeout`
|
|
254
|
+
(5 seconds, editable), so a busy table fails fast instead of queueing behind other locks.
|
|
255
|
+
|
|
256
|
+
Anything that drops something (a table, column, constraint or index) lists what depends on it first
|
|
257
|
+
(views, foreign keys from other tables, functions), adds `CASCADE` only when ticked, and asks for two
|
|
258
|
+
random words before it runs, as write mode does. **Drop table** is a separate button.
|
|
259
|
+
|
|
260
|
+
The page needs `designer: true`, which defaults to on for the `development` and `local` tiers; where it
|
|
261
|
+
is off, or the environment is read only (unless it is protected and switched to write mode), the SQL
|
|
262
|
+
can only be previewed. The search box (`Cmd/Ctrl-K`) has **New table** and `edit design of orders`.
|
|
263
|
+
|
|
264
|
+
### Compare page
|
|
265
|
+
|
|
266
|
+
A **Compare** tab shows how this environment's database is built differently from another's, for
|
|
267
|
+
checking that staging matches production before a release (`#/staging/compare/production`). It
|
|
268
|
+
compares every schema, or one, which may have another name on the other side (`shop` against
|
|
269
|
+
`shop_next`). Tables with their columns, indexes, constraints, triggers and row level security
|
|
270
|
+
policies, views, materialized views, functions, sequences, types and extensions are each listed as
|
|
271
|
+
differing, or only on one side; what differs is shown side by side, and view and function
|
|
272
|
+
definitions line by line. Owners, grants, comments, column order and the objects an extension
|
|
273
|
+
creates are not compared. Filter by name or by kind of difference, open either side's object, or
|
|
274
|
+
copy the whole comparison as text. Only the catalogs are read, so it works on read only
|
|
275
|
+
environments, and a password the other environment asks for is asked on this page.
|
|
276
|
+
|
|
277
|
+
## Commands
|
|
278
|
+
|
|
279
|
+
```sh
|
|
280
|
+
psql-explorer # the interactive TUI
|
|
281
|
+
psql-explorer envs # the environments you can use
|
|
282
|
+
psql-explorer schemas --env staging
|
|
283
|
+
psql-explorer tables --env staging [--schema app]
|
|
284
|
+
psql-explorer describe --env staging users # or app.users
|
|
285
|
+
psql-explorer rows --env staging users --limit 20 --order created_at --desc
|
|
286
|
+
psql-explorer query --env staging "select count(*) from orders"
|
|
287
|
+
echo "select now()" | psql-explorer query --env staging
|
|
288
|
+
psql-explorer info --env staging
|
|
289
|
+
psql-explorer history --env staging
|
|
290
|
+
psql-explorer diff --env staging --with production [--schema app] [--with-schema app_v2]
|
|
291
|
+
psql-explorer configgen:file # a commented example environment file
|
|
292
|
+
psql-explorer configgen:vault
|
|
293
|
+
psql-explorer secret:set prod-password # for `source: stored`; asks, or reads stdin
|
|
294
|
+
psql-explorer secret:delete prod-password
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
Every command prints a table like psql's; `--format csv` or `--format json` (or `--json`) give
|
|
298
|
+
output for other tools. In JSON, numbers that fit a JavaScript number, booleans and `json`/`jsonb`
|
|
299
|
+
values are themselves; `bigint`, `numeric` and everything else are the text Postgres sent, so no
|
|
300
|
+
digit is lost.
|
|
301
|
+
|
|
302
|
+
| Option | Used for |
|
|
303
|
+
| ----------------- | --------------------------------------------------------------------------- |
|
|
304
|
+
| `--config <base>` | the shared config's base path; default `$PSQL_EXPLORER_ENVIRONMENTS` |
|
|
305
|
+
| `--env <name>` | the environment: a shared one, else a local connection of that name |
|
|
306
|
+
| `--schema <name>` | `tables`, `describe`, `rows`: the schema; default the environment's |
|
|
307
|
+
| `--format <f>` | `table` (default), `csv` or `json` |
|
|
308
|
+
| `--limit <n>` | `rows`: rows per page (default 100); `history`: how many (default 50) |
|
|
309
|
+
| `--offset <n>` | `rows`: rows to skip |
|
|
310
|
+
| `--order <cols>` | `rows`: sort by these columns, comma separated; `--desc` for descending |
|
|
311
|
+
| `--max-rows <n>` | `query`: read at most this many rows (default 1000), the rest are not read |
|
|
312
|
+
| `--timeout <s>` | the server cancels a statement after this many seconds (default 30, 0 none) |
|
|
313
|
+
|
|
314
|
+
Exit codes: `0` done, `1` failed (the reason is on stderr), `2` wrong command line. A value the
|
|
315
|
+
config says to `prompt` for is asked for on the terminal (a secret without echo); with no terminal
|
|
316
|
+
the command fails and names the value.
|
|
317
|
+
|
|
318
|
+
`query` runs one statement at a time. Each statement is kept in the history of that environment
|
|
319
|
+
(in your local data, see below), with how long it took and how many rows it returned.
|
|
320
|
+
|
|
321
|
+
## The TUI
|
|
322
|
+
|
|
323
|
+
The **Environments** screen lists the shared environments and your local connections, with their
|
|
324
|
+
tier, address, TLS mode and whether they are read only. `Enter` explores one, `t` tests the
|
|
325
|
+
connection, `n` adds a local connection, `e` and `d` edit and delete one.
|
|
326
|
+
|
|
327
|
+
Exploring an environment has four tabs (`1`–`4`, or `Tab`):
|
|
328
|
+
|
|
329
|
+
1. **Tables** — the tables, views and materialized views of a schema, with row estimates, sizes
|
|
330
|
+
and comments. `/` filters, `s` picks another schema (remembered per environment), `Enter` opens
|
|
331
|
+
a table on its **Rows** (a page at a time, sorted by the primary key), its **Structure**
|
|
332
|
+
(columns, types, defaults) and its **Indexes & constraints**; `Tab` switches between them.
|
|
333
|
+
In the rows, `n`/`p` page, `o` sorts by the selected column (again for descending, a third time
|
|
334
|
+
for the default), `c` counts the rows exactly, and `Enter` shows a row field by field.
|
|
335
|
+
2. **Query** — type a statement and press `Enter`; `↑`/`↓` recall earlier ones. The result is a
|
|
336
|
+
grid like the rows. `Esc` leaves the editor, `e` returns to it, `r` runs it again.
|
|
337
|
+
3. **History** — what was run against this environment, newest first. `Enter` puts a statement
|
|
338
|
+
back in the Query tab.
|
|
339
|
+
4. **Info** — the server version, database size and session settings, and the other connections
|
|
340
|
+
to the database.
|
|
341
|
+
|
|
342
|
+
In every grid the arrow keys (or `h` `j` `k` `l`) move a cell at a time, `PgUp`/`PgDn` a page,
|
|
343
|
+
`g`/`G` to the first and last row, `0`/`$` to the first and last column. `Esc` goes back, `q`
|
|
344
|
+
quits (when not typing), and so does `Ctrl+C`.
|
|
345
|
+
|
|
346
|
+
## Read only, and other safety
|
|
347
|
+
|
|
348
|
+
- An environment with `readOnly: true` opens every session with
|
|
349
|
+
`default_transaction_read_only = on`, so the server refuses `INSERT`, `UPDATE`, `DELETE`, DDL
|
|
350
|
+
and the like. `readOnly` defaults to `protected`, which defaults to true for the `production`
|
|
351
|
+
tier. The header shows **READ ONLY**. This guards against mistakes; it is not a permission
|
|
352
|
+
system, so give people a database role that can only read where it matters (Vault's database
|
|
353
|
+
secrets engine is a good way to hand those out, see below).
|
|
354
|
+
- An environment with `confirm: true` makes you type back two random words (for example
|
|
355
|
+
`maple ticket`) before it is opened, in the TUI, the web UI and on the terminal, so a protected
|
|
356
|
+
environment is never entered by accident, even when its password is stored in the config.
|
|
357
|
+
`confirm` defaults to `protected`; set `confirm: false` to turn it off. It is asked once per run
|
|
358
|
+
of the program, nothing is looked up before it is answered, and it fails (rather than skips)
|
|
359
|
+
when nobody can be asked, e.g. in a script with no terminal. The web UI also shows a banner
|
|
360
|
+
above the SQL box on a protected environment.
|
|
361
|
+
- In the web UI, the SQL box of a protected, read only environment has a switch at its top right.
|
|
362
|
+
Turning it on asks for two random words again (always, whatever `confirm` says), then every
|
|
363
|
+
connection of that environment is opened read write until you switch it off or stop the server.
|
|
364
|
+
The banner turns red and pulses while it is on. Turning it off needs no words.
|
|
365
|
+
- Every statement has a time limit (`statement_timeout`, 30 s unless `--timeout` says otherwise),
|
|
366
|
+
and a query reads at most `--max-rows` rows (1000 in the TUI), so a `SELECT *` on a big table
|
|
367
|
+
does not fill your memory.
|
|
368
|
+
- Values are shown as the text Postgres sends: no time zone conversion, no rounding.
|
|
369
|
+
|
|
370
|
+
## Configuration
|
|
371
|
+
|
|
372
|
+
### Environment variables
|
|
373
|
+
|
|
374
|
+
| Variable | Used for |
|
|
375
|
+
| ---------------------------------- | ------------------------------------------------------------------------------------------------ |
|
|
376
|
+
| `PSQL_EXPLORER_ENVIRONMENTS` | the shared config's base path, a directory or `vault:<path>`; `--config` wins |
|
|
377
|
+
| `PSQL_EXPLORER_CONFIG` | the directory holding `explorer.db` (local connections, stored secrets, settings, query history) |
|
|
378
|
+
| `XDG_CONFIG_HOME` | without `PSQL_EXPLORER_CONFIG`, `explorer.db` is in `$XDG_CONFIG_HOME/psql-explorer` |
|
|
379
|
+
| | (else `~/.config/psql-explorer`) |
|
|
380
|
+
| `VAULT_ADDR` | Vault's address (required for anything in Vault) |
|
|
381
|
+
| `VAULT_TOKEN` | the Vault token to use |
|
|
382
|
+
| `VAULT_ROLE_ID`, `VAULT_SECRET_ID` | log in to Vault through AppRole |
|
|
383
|
+
| `VAULT_JWT`, `VAULT_ROLE` | log in to Vault with a JWT / OIDC token |
|
|
384
|
+
| `VAULT_NAMESPACE` | the Vault Enterprise namespace |
|
|
385
|
+
| any name | read by an environment's `source: env` values, for example `STAGING_DB_PASSWORD` |
|
|
386
|
+
|
|
387
|
+
Empty values count as unset. `explorer.db` is created readable by its owner only.
|
|
388
|
+
|
|
389
|
+
### Shared configuration
|
|
390
|
+
|
|
391
|
+
Environments shared by a team live under a **base path**, given with `--config <base>` or
|
|
392
|
+
`$PSQL_EXPLORER_ENVIRONMENTS`. It holds one folder per environment, each with an `explorer.yaml`:
|
|
393
|
+
|
|
394
|
+
```
|
|
395
|
+
<base>/staging/explorer.yaml one environment; the folder name is its name
|
|
396
|
+
<base>/production/explorer.yaml
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
The base path is a directory (a git checkout, a shared drive, ...) or Vault:
|
|
400
|
+
|
|
401
|
+
```sh
|
|
402
|
+
psql-explorer --config ~/team/explorer-config
|
|
403
|
+
psql-explorer --config vault:secret/data/psql-explorer # a KV v2 "<mount>/data/<path>"
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
Environments are found by listing the base path. A folder without an `explorer.yaml` is ignored.
|
|
407
|
+
An environment file that is wrong is reported (on the Environments screen, or on stderr) and left
|
|
408
|
+
out; the others still load.
|
|
409
|
+
|
|
410
|
+
#### Config in Vault
|
|
411
|
+
|
|
412
|
+
In Vault each environment is the `explorer.yaml` key of a secret below the base path:
|
|
413
|
+
`secret/data/psql-explorer/staging` is the `staging` environment. psql-explorer lists
|
|
414
|
+
`secret/metadata/psql-explorer` to find them, so **what a person sees follows their Vault policy**:
|
|
415
|
+
an environment they may not read is simply not in their list.
|
|
416
|
+
|
|
417
|
+
```sh
|
|
418
|
+
vault kv put secret/psql-explorer/staging explorer.yaml=@staging.yaml
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
```hcl
|
|
422
|
+
path "secret/metadata/psql-explorer" { capabilities = ["list"] }
|
|
423
|
+
path "secret/data/psql-explorer/staging" { capabilities = ["read"] } # one per environment
|
|
424
|
+
# or: path "secret/data/psql-explorer/*" { capabilities = ["read"] }
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
### An environment file
|
|
428
|
+
|
|
429
|
+
`psql-explorer configgen:file` (or `configgen:vault`) prints a fully commented example. In short:
|
|
430
|
+
|
|
431
|
+
```yaml
|
|
432
|
+
tier: production # production | reference | demo | development | local
|
|
433
|
+
protected: true # optional; default: tier is production
|
|
434
|
+
readOnly: true # optional; default: protected
|
|
435
|
+
confirm: true # optional; default: protected (type two random words to open it)
|
|
436
|
+
seeder: false # optional; default: tier is development or local (the web UI's Seed page)
|
|
437
|
+
designer: false # optional; default: tier is development or local (the web UI's Design page runs SQL)
|
|
438
|
+
host: db.prod.internal
|
|
439
|
+
port: 5432
|
|
440
|
+
database: app
|
|
441
|
+
schema: app # optional; where browsing starts, and first on the search_path; default public
|
|
442
|
+
username: readonly
|
|
443
|
+
password: # or `auth: none` for a server that takes no password
|
|
444
|
+
source: vault
|
|
445
|
+
value: secret/data/db/prod#password
|
|
446
|
+
tls:
|
|
447
|
+
mode: verify-full # disable | require | verify-ca | verify-full
|
|
448
|
+
ca: {source: file, value: ./ca.pem}
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
Any of `host`, `port`, `database`, `schema`, `username`, `password` and the TLS `ca`, `cert`,
|
|
452
|
+
`key` and `keyPassphrase` can be written as plain text or as a `source` and a `value`:
|
|
453
|
+
|
|
454
|
+
| `source` | the value is |
|
|
455
|
+
| --------------------- | ---------------------------------------------------------------------------------- |
|
|
456
|
+
| `plaintext`, `inline` | the text in `value` |
|
|
457
|
+
| `env` | the environment variable named in `value` |
|
|
458
|
+
| `file` | the content of the file in `value`, relative to the environment's folder |
|
|
459
|
+
| `stored` | a secret of that name in your local store (`psql-explorer secret:set <name>`) |
|
|
460
|
+
| `prompt` | asked for when connecting (no `value`), and remembered until the program ends |
|
|
461
|
+
| `vault` | a key of a KV v2 secret: `value: <data path>#<key>`, e.g. `secret/data/db/prod#pw` |
|
|
462
|
+
|
|
463
|
+
### Vault
|
|
464
|
+
|
|
465
|
+
Vault needs `VAULT_ADDR`, and a token from the first of: `VAULT_TOKEN`; an AppRole login with
|
|
466
|
+
`VAULT_ROLE_ID` and `VAULT_SECRET_ID`; a JWT login with `VAULT_JWT` and `VAULT_ROLE`; the
|
|
467
|
+
`~/.vault-token` the `vault` CLI writes on `vault login`. Only plain KV v2 secrets are read, not
|
|
468
|
+
dynamic ones such as the database or PKI engines. Secrets are read each time they are needed and
|
|
469
|
+
never kept, so a rotated password is picked up at once. A batch of queries that runs side by side
|
|
470
|
+
(the value search reads tables on up to 4 connections) reads them once and shares the result. An expired token is reported
|
|
471
|
+
as such, with a reminder to `vault login`.
|
|
472
|
+
|
|
473
|
+
### Local connections
|
|
474
|
+
|
|
475
|
+
Press `n` on the Environments screen to save a connection of your own (host, port, database,
|
|
476
|
+
username, password, schema and SSL). They live in `explorer.db` with the password unencrypted,
|
|
477
|
+
in a file only you can read, are never read only, and can be used by name with `--env`.
|
|
478
|
+
|
|
479
|
+
## Development
|
|
480
|
+
|
|
481
|
+
```sh
|
|
482
|
+
npm install
|
|
483
|
+
npm run build # compile src/ to dist/ with tsc
|
|
484
|
+
node dist/cli.js # run the built CLI
|
|
485
|
+
npm test # run the tests (Vitest; builds dist/ first)
|
|
486
|
+
npm run lint # typecheck + ESLint + Prettier check
|
|
487
|
+
npm run format # apply Prettier formatting
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
`npm run dev` starts psql-explorer against a demo: a `demo_shop` database (customers, products,
|
|
491
|
+
orders, a 200,000-row `page_views` table with no index on `customer_id` for trying Explain, a view,
|
|
492
|
+
a materialized view and an `audit` schema) on the `psql-explorer-pg` container
|
|
493
|
+
(port 55440, started if it is not running; set `DEMO_PG_URL` to use another server), and a shared
|
|
494
|
+
config in `.demo/config` with a read-write, a read-only and a protected environment. Its data
|
|
495
|
+
directory is `.demo/data`, so your real `~/.config/psql-explorer` is untouched.
|
|
496
|
+
`npm run dev:web` starts the web UI against the same demo. `npm run dev:reset` and
|
|
497
|
+
`npm run dev:web:reset` build the demo again. `npm run dev:plain` runs the TUI without it.
|
|
498
|
+
|
|
499
|
+
The tests that need a real Postgres run when `PSQL_EXPLORER_TEST_PG_URL` is set, and are skipped
|
|
500
|
+
otherwise:
|
|
501
|
+
|
|
502
|
+
```sh
|
|
503
|
+
PSQL_EXPLORER_TEST_PG_URL=postgres://postgres:postgres@localhost:55440/app npm test
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
Each run creates a schema of its own (`explorer_test_<random>`) and drops it afterwards.
|
package/RELEASES.md
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Release notes
|
|
2
|
+
|
|
3
|
+
## 0.3.0 (2026-11-02)
|
|
4
|
+
|
|
5
|
+
### 2026-11-01
|
|
6
|
+
|
|
7
|
+
- [cli] Add a --relnotes flag that prints the release notes
|
|
8
|
+
- Fix: Do not draw nodes for the null side of an outer join in the graph
|
|
9
|
+
- Fix: Leave out rows whose chosen label is null from the row graph
|
|
10
|
+
|
|
11
|
+
## 0.2.0 (2026-11-01)
|
|
12
|
+
|
|
13
|
+
Everything built so far, from the commit summaries, oldest first. Print it with `psql-explorer --relnotes`.
|
|
14
|
+
|
|
15
|
+
### 2026-10-28
|
|
16
|
+
|
|
17
|
+
- [cli] Initial psql-explorer CLI and TUI
|
|
18
|
+
|
|
19
|
+
### 2026-10-29
|
|
20
|
+
|
|
21
|
+
- Database view with schema navigation and object details
|
|
22
|
+
- SQL editor with paged results, ERD view and themes
|
|
23
|
+
- Explain button describing a query plan in plain English
|
|
24
|
+
- Answer prompted config values in the browser
|
|
25
|
+
- Confirm protected environments with two random words
|
|
26
|
+
- Switch protected databases to write mode from the SQL box
|
|
27
|
+
|
|
28
|
+
### 2026-10-30
|
|
29
|
+
|
|
30
|
+
- Filter, follow keys, inspect rows and profile columns
|
|
31
|
+
- Open a table as a pre-filled query in the SQL box
|
|
32
|
+
- Deep links carry the SQL statement, tab and page, and refuse unknown databases
|
|
33
|
+
- [tui] Serve the web UI from the TUI and show where it is running
|
|
34
|
+
- Fix: Keep the address bounded so a huge statement cannot stop a query from running
|
|
35
|
+
- Open several query tabs on the SQL page and rename them
|
|
36
|
+
- Copy and share buttons on the SQL page, and paste a share code to open it
|
|
37
|
+
- Numbered pages and a go-to-page box on results pages
|
|
38
|
+
- Export results, query history and saved queries, and cancel with timing
|
|
39
|
+
- Result charts with image export, and inline row editing in write mode
|
|
40
|
+
- Group by day, month and more of date columns, with a time zone setting
|
|
41
|
+
- Format SQL, run scripts statement by statement, and read special types
|
|
42
|
+
- Table stats, an explain plan tree and a dependency view
|
|
43
|
+
- A Seed page that fills tables with generated rows
|
|
44
|
+
- Fix: Only pop up questions for the environment the page is on
|
|
45
|
+
- Skip seeded rows that run out of unique values
|
|
46
|
+
- Link seeded rows to other tables, and make rows for each of their rows
|
|
47
|
+
- Tick and weight the allowed values of a seeded column
|
|
48
|
+
- Compare schemas between environments on a Compare page and with diff
|
|
49
|
+
- Give the dev demo environment a database with a different schema
|
|
50
|
+
- Docs: Plan a record graph and a table designer in TASKS2.md
|
|
51
|
+
- Add a designer setting for environments
|
|
52
|
+
- Add a graph API for rows linked by foreign keys
|
|
53
|
+
- Add a DDL builder for table designs
|
|
54
|
+
- Add a Graph page that draws rows linked by foreign keys
|
|
55
|
+
- Add Graph entry points, a shareable address and export
|
|
56
|
+
|
|
57
|
+
### 2026-10-31
|
|
58
|
+
|
|
59
|
+
- Add a Design page that creates tables from a form
|
|
60
|
+
- Edit and drop tables from the Design page
|
|
61
|
+
- Docs: Document the Design page and add New table and Edit design to the search box
|
|
62
|
+
- Add a Cypher page that queries foreign keys as a graph, and rework the Graph page
|
|
63
|
+
- Add a Settings editor for GUCs on the SQL page
|
|
64
|
+
|
|
65
|
+
### 2026-11-01
|
|
66
|
+
|
|
67
|
+
- Explain functions and column defaults when hovering SQL
|
|
68
|
+
- Autocomplete function names in the SQL editor
|
|
69
|
+
- Show parameter help for the function call being written in the SQL editor
|
|
70
|
+
- Update the sidebar when DDL changes the database objects
|
|
71
|
+
- Autocomplete column types in CREATE TABLE and split the bundle
|
|
72
|
+
- Autocomplete columns in the column list of CREATE INDEX
|
|
73
|
+
- Autocomplete tables after ON and REFERENCES and columns in more DDL
|
|
74
|
+
- Add an LCARS theme with console sounds, a star chart graph and block navs
|
|
75
|
+
- Show row level security policies on tables and the roles of the server
|
|
76
|
+
- Show security details for views and functions
|
|
77
|
+
- Add a dark variant of the standard theme
|
|
78
|
+
- Fix: Share the standard title styling with the dark variant
|
|
79
|
+
- Give each theme its own splash text
|
|
80
|
+
- Add a Build page that makes SELECT queries from a form
|
|
81
|
+
- Drop a file onto a field in the row editor
|
|
82
|
+
- Raise the dropped file limit to 20 MB
|
|
83
|
+
- Fix: Accept a dropped file in an edit request
|
|
84
|
+
- Add a download link for bytea fields in the row viewer
|
|
85
|
+
- Detect more file types and offer a gzip download for bytea
|
|
86
|
+
- Fix: [cli] List confirm in the configgen examples
|
|
87
|
+
- Add a warp field behind the environment list in the lcars theme
|
package/dist/app.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useState, useSyncExternalStore } from 'react';
|
|
3
|
+
import { Box, useApp, useInput, useStdin } from 'ink';
|
|
4
|
+
import { screens as defaultScreens, } from './screens/index.js';
|
|
5
|
+
import PromptModal from './screens/prompt-modal.js';
|
|
6
|
+
import { useTerminalSize } from './screens/terminal.js';
|
|
7
|
+
const noSubscribe = () => () => undefined;
|
|
8
|
+
const noPrompt = () => undefined;
|
|
9
|
+
export default function App({ version, services, initialScreen = 'splash', initialParams, fullScreen = false, screens = defaultScreens, }) {
|
|
10
|
+
const [route, setRoute] = useState({
|
|
11
|
+
name: initialScreen,
|
|
12
|
+
params: initialParams,
|
|
13
|
+
key: 0,
|
|
14
|
+
});
|
|
15
|
+
const { exit } = useApp();
|
|
16
|
+
const { columns, rows } = useTerminalSize();
|
|
17
|
+
const { isRawModeSupported } = useStdin();
|
|
18
|
+
const navigate = useCallback((name, ...[params]) => {
|
|
19
|
+
setRoute((previous) => ({ name, params, key: previous.key + 1 }));
|
|
20
|
+
}, []);
|
|
21
|
+
const entry = screens[route.name];
|
|
22
|
+
const { prompts } = services;
|
|
23
|
+
const prompt = useSyncExternalStore(prompts ? prompts.subscribe : noSubscribe, prompts ? prompts.current : noPrompt);
|
|
24
|
+
const screenInput = isRawModeSupported && !prompt;
|
|
25
|
+
// Global quit key, off on screens that take text input; Ctrl+C is handled by Ink (exitOnCtrlC).
|
|
26
|
+
useInput((input) => {
|
|
27
|
+
if (input === 'q')
|
|
28
|
+
exit();
|
|
29
|
+
}, { isActive: screenInput && !entry.capturesInput });
|
|
30
|
+
// The registry ties each component to its own params type; the route carries the params that
|
|
31
|
+
// navigate() type-checked for this screen, so widening here is safe.
|
|
32
|
+
const Screen = entry.component;
|
|
33
|
+
const screen = (_jsx(Screen, { version: version, navigate: navigate, params: route.params, services: services, isInputActive: screenInput }, route.key));
|
|
34
|
+
const modal = prompt && (_jsx(PromptModal, { prompt: prompt, isInputActive: isRawModeSupported }, `${prompt.envName}.${prompt.field}`));
|
|
35
|
+
if (!fullScreen) {
|
|
36
|
+
return (_jsxs(_Fragment, { children: [screen, modal] }));
|
|
37
|
+
}
|
|
38
|
+
return (_jsxs(Box, { width: columns, height: rows, flexDirection: "column", justifyContent: prompt ? 'center' : 'flex-start', children: [_jsx(Box, { display: prompt ? 'none' : 'flex', flexDirection: "column", flexGrow: 1, children: screen }), modal] }));
|
|
39
|
+
}
|