@kazurolabs/psql-explorer 0.1.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +229 -18
- package/RELEASES.md +95 -0
- package/dist/cli.js +57 -13
- package/dist/commands/configgen.js +25 -25
- package/dist/commands/explore.js +86 -4
- package/dist/config/load.js +11 -73
- package/dist/config/resolve-environment.js +10 -62
- package/dist/config/resolvers/index.js +1 -1
- package/dist/config/resolvers/prompt.js +2 -2
- package/dist/config/resolvers/vault.js +1 -83
- package/dist/db/store.js +57 -0
- package/dist/environments.js +5 -2
- package/dist/explorer/alter.js +493 -0
- package/dist/explorer/catalog.js +31 -0
- package/dist/explorer/connect-entry.js +29 -18
- 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 +44 -3
- package/dist/explorer/filter.js +121 -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 +100 -13
- 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 +25 -1
- package/dist/explorer/stats.js +135 -0
- package/dist/explorer/timeout.js +13 -0
- package/dist/screens/environments.js +6 -3
- package/dist/screens/explorer.js +0 -3
- package/dist/screens/splash.js +4 -2
- package/dist/web/server.js +655 -22
- package/package.json +4 -1
- 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-BiFx4Lyw.js +1 -0
- package/web/dist/assets/code-xml-CPEjNFOw.js +1 -0
- package/web/dist/assets/compare-view-COQ3yE76.js +2 -0
- package/web/dist/assets/copy-C2N9tUYJ.js +1 -0
- package/web/dist/assets/cypher-view-BzyL4mNU.js +13 -0
- package/web/dist/assets/design-view-BPm7dSTY.js +9 -0
- package/web/dist/assets/erd-view-DqGDws2_.js +1 -0
- package/web/dist/assets/format-sql-CoddYUOF.js +5 -0
- package/web/dist/assets/graph-force-ChCGXQUt.js +1 -0
- package/web/dist/assets/graph-link-C8P9s_0W.js +1 -0
- package/web/dist/assets/graph-view-BpoUrUCP.js +3 -0
- package/web/dist/assets/index-CpHnjRLv.js +10 -0
- package/web/dist/assets/index-OzJqnJWG.css +2 -0
- package/web/dist/assets/key-round-JiGCgCWS.js +1 -0
- package/web/dist/assets/loader-circle-BxDJOoJO.js +1 -0
- package/web/dist/assets/query-builder-view-CtRexMEX.js +4 -0
- package/web/dist/assets/rotate-ccw-CwdA-J3t.js +1 -0
- package/web/dist/assets/route-7eu-iAqD.js +15 -0
- package/web/dist/assets/seed-view-BIkBTiU6.js +7 -0
- package/web/dist/assets/shield-alert-keJIAND5.js +1 -0
- package/web/dist/assets/sliders-horizontal-DKtL_ql7.js +1 -0
- package/web/dist/assets/sql-tabs-DUSXdYCr.js +2 -0
- package/web/dist/assets/table-Drc_n1gF.js +1 -0
- package/web/dist/assets/triangle-alert-C5nF69rW.js +1 -0
- package/web/dist/assets/zone-CHDOrDBF.js +5 -0
- package/web/dist/index.html +9 -2
- package/web/dist/assets/index-BMLFCtFY.js +0 -17
- package/web/dist/assets/index-CMXJRL9O.css +0 -2
package/README.md
CHANGED
|
@@ -52,6 +52,228 @@ their tables, views, functions, sequences and types, and the one you pick is sho
|
|
|
52
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
53
|
`npm run install:web` once first), and `npm run build` compiles the CLI and builds `web/dist`.
|
|
54
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
|
+
|
|
55
277
|
## Commands
|
|
56
278
|
|
|
57
279
|
```sh
|
|
@@ -65,6 +287,7 @@ psql-explorer query --env staging "select count(*) from orders"
|
|
|
65
287
|
echo "select now()" | psql-explorer query --env staging
|
|
66
288
|
psql-explorer info --env staging
|
|
67
289
|
psql-explorer history --env staging
|
|
290
|
+
psql-explorer diff --env staging --with production [--schema app] [--with-schema app_v2]
|
|
68
291
|
psql-explorer configgen:file # a commented example environment file
|
|
69
292
|
psql-explorer configgen:vault
|
|
70
293
|
psql-explorer secret:set prod-password # for `source: stored`; asks, or reads stdin
|
|
@@ -143,8 +366,6 @@ quits (when not typing), and so does `Ctrl+C`.
|
|
|
143
366
|
and a query reads at most `--max-rows` rows (1000 in the TUI), so a `SELECT *` on a big table
|
|
144
367
|
does not fill your memory.
|
|
145
368
|
- Values are shown as the text Postgres sends: no time zone conversion, no rounding.
|
|
146
|
-
- Short-lived credentials and certificates issued by Vault are revoked when you leave the
|
|
147
|
-
environment or the command ends.
|
|
148
369
|
|
|
149
370
|
## Configuration
|
|
150
371
|
|
|
@@ -212,6 +433,8 @@ tier: production # production | reference | demo | development | local
|
|
|
212
433
|
protected: true # optional; default: tier is production
|
|
213
434
|
readOnly: true # optional; default: protected
|
|
214
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)
|
|
215
438
|
host: db.prod.internal
|
|
216
439
|
port: 5432
|
|
217
440
|
database: app
|
|
@@ -237,26 +460,14 @@ Any of `host`, `port`, `database`, `schema`, `username`, `password` and the TLS
|
|
|
237
460
|
| `prompt` | asked for when connecting (no `value`), and remembered until the program ends |
|
|
238
461
|
| `vault` | a key of a KV v2 secret: `value: <data path>#<key>`, e.g. `secret/data/db/prod#pw` |
|
|
239
462
|
|
|
240
|
-
Vault can also issue the login and the client certificate:
|
|
241
|
-
|
|
242
|
-
```yaml
|
|
243
|
-
credentials: # instead of username and password
|
|
244
|
-
source: vault-database
|
|
245
|
-
value: database/creds/readonly
|
|
246
|
-
tls:
|
|
247
|
-
mode: verify-full
|
|
248
|
-
client: # instead of cert and key (and ca, unless given)
|
|
249
|
-
source: vault-pki
|
|
250
|
-
value: pki/issue/psql-explorer
|
|
251
|
-
commonName: psql-explorer
|
|
252
|
-
```
|
|
253
|
-
|
|
254
463
|
### Vault
|
|
255
464
|
|
|
256
465
|
Vault needs `VAULT_ADDR`, and a token from the first of: `VAULT_TOKEN`; an AppRole login with
|
|
257
466
|
`VAULT_ROLE_ID` and `VAULT_SECRET_ID`; a JWT login with `VAULT_JWT` and `VAULT_ROLE`; the
|
|
258
|
-
`~/.vault-token` the `vault` CLI writes on `vault login`.
|
|
259
|
-
|
|
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
|
|
260
471
|
as such, with a reminder to `vault login`.
|
|
261
472
|
|
|
262
473
|
### Local connections
|
package/RELEASES.md
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# Release notes
|
|
2
|
+
|
|
3
|
+
## 0.3.1 (2026-11-02)
|
|
4
|
+
|
|
5
|
+
### 2026-11-02
|
|
6
|
+
|
|
7
|
+
- Fix: Send explain as one statement in a rolled-back transaction, so text after a `;` can no longer write to a read-only environment
|
|
8
|
+
- Fix: Make SQL page links wait for a click before running or explaining their statement
|
|
9
|
+
- Fix: Run a query that cannot be paged as written only when the person pressed Run on it, and page a query that ends in `; -- comment`
|
|
10
|
+
|
|
11
|
+
## 0.3.0 (2026-11-02)
|
|
12
|
+
|
|
13
|
+
### 2026-11-01
|
|
14
|
+
|
|
15
|
+
- [cli] Add a --relnotes flag that prints the release notes
|
|
16
|
+
- Fix: Do not draw nodes for the null side of an outer join in the graph
|
|
17
|
+
- Fix: Leave out rows whose chosen label is null from the row graph
|
|
18
|
+
|
|
19
|
+
## 0.2.0 (2026-11-01)
|
|
20
|
+
|
|
21
|
+
Everything built so far, from the commit summaries, oldest first. Print it with `psql-explorer --relnotes`.
|
|
22
|
+
|
|
23
|
+
### 2026-10-28
|
|
24
|
+
|
|
25
|
+
- [cli] Initial psql-explorer CLI and TUI
|
|
26
|
+
|
|
27
|
+
### 2026-10-29
|
|
28
|
+
|
|
29
|
+
- Database view with schema navigation and object details
|
|
30
|
+
- SQL editor with paged results, ERD view and themes
|
|
31
|
+
- Explain button describing a query plan in plain English
|
|
32
|
+
- Answer prompted config values in the browser
|
|
33
|
+
- Confirm protected environments with two random words
|
|
34
|
+
- Switch protected databases to write mode from the SQL box
|
|
35
|
+
|
|
36
|
+
### 2026-10-30
|
|
37
|
+
|
|
38
|
+
- Filter, follow keys, inspect rows and profile columns
|
|
39
|
+
- Open a table as a pre-filled query in the SQL box
|
|
40
|
+
- Deep links carry the SQL statement, tab and page, and refuse unknown databases
|
|
41
|
+
- [tui] Serve the web UI from the TUI and show where it is running
|
|
42
|
+
- Fix: Keep the address bounded so a huge statement cannot stop a query from running
|
|
43
|
+
- Open several query tabs on the SQL page and rename them
|
|
44
|
+
- Copy and share buttons on the SQL page, and paste a share code to open it
|
|
45
|
+
- Numbered pages and a go-to-page box on results pages
|
|
46
|
+
- Export results, query history and saved queries, and cancel with timing
|
|
47
|
+
- Result charts with image export, and inline row editing in write mode
|
|
48
|
+
- Group by day, month and more of date columns, with a time zone setting
|
|
49
|
+
- Format SQL, run scripts statement by statement, and read special types
|
|
50
|
+
- Table stats, an explain plan tree and a dependency view
|
|
51
|
+
- A Seed page that fills tables with generated rows
|
|
52
|
+
- Fix: Only pop up questions for the environment the page is on
|
|
53
|
+
- Skip seeded rows that run out of unique values
|
|
54
|
+
- Link seeded rows to other tables, and make rows for each of their rows
|
|
55
|
+
- Tick and weight the allowed values of a seeded column
|
|
56
|
+
- Compare schemas between environments on a Compare page and with diff
|
|
57
|
+
- Give the dev demo environment a database with a different schema
|
|
58
|
+
- Docs: Plan a record graph and a table designer in TASKS2.md
|
|
59
|
+
- Add a designer setting for environments
|
|
60
|
+
- Add a graph API for rows linked by foreign keys
|
|
61
|
+
- Add a DDL builder for table designs
|
|
62
|
+
- Add a Graph page that draws rows linked by foreign keys
|
|
63
|
+
- Add Graph entry points, a shareable address and export
|
|
64
|
+
|
|
65
|
+
### 2026-10-31
|
|
66
|
+
|
|
67
|
+
- Add a Design page that creates tables from a form
|
|
68
|
+
- Edit and drop tables from the Design page
|
|
69
|
+
- Docs: Document the Design page and add New table and Edit design to the search box
|
|
70
|
+
- Add a Cypher page that queries foreign keys as a graph, and rework the Graph page
|
|
71
|
+
- Add a Settings editor for GUCs on the SQL page
|
|
72
|
+
|
|
73
|
+
### 2026-11-01
|
|
74
|
+
|
|
75
|
+
- Explain functions and column defaults when hovering SQL
|
|
76
|
+
- Autocomplete function names in the SQL editor
|
|
77
|
+
- Show parameter help for the function call being written in the SQL editor
|
|
78
|
+
- Update the sidebar when DDL changes the database objects
|
|
79
|
+
- Autocomplete column types in CREATE TABLE and split the bundle
|
|
80
|
+
- Autocomplete columns in the column list of CREATE INDEX
|
|
81
|
+
- Autocomplete tables after ON and REFERENCES and columns in more DDL
|
|
82
|
+
- Add an LCARS theme with console sounds, a star chart graph and block navs
|
|
83
|
+
- Show row level security policies on tables and the roles of the server
|
|
84
|
+
- Show security details for views and functions
|
|
85
|
+
- Add a dark variant of the standard theme
|
|
86
|
+
- Fix: Share the standard title styling with the dark variant
|
|
87
|
+
- Give each theme its own splash text
|
|
88
|
+
- Add a Build page that makes SELECT queries from a form
|
|
89
|
+
- Drop a file onto a field in the row editor
|
|
90
|
+
- Raise the dropped file limit to 20 MB
|
|
91
|
+
- Fix: Accept a dropped file in an edit request
|
|
92
|
+
- Add a download link for bytea fields in the row viewer
|
|
93
|
+
- Detect more file types and offer a gzip download for bytea
|
|
94
|
+
- Fix: [cli] List confirm in the configgen examples
|
|
95
|
+
- Add a warp field behind the environment list in the lcars theme
|
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
3
4
|
import { join } from 'node:path';
|
|
4
5
|
import { render } from 'ink';
|
|
5
6
|
import meow from 'meow';
|
|
@@ -20,7 +21,7 @@ import { createWebServer, DEFAULT_WEB_PORT, listen, openBrowser } from './web/se
|
|
|
20
21
|
// a single source of truth. It handles --help and --version (exit 0) itself.
|
|
21
22
|
const cli = meow(`
|
|
22
23
|
Usage
|
|
23
|
-
$ psql-explorer start the interactive TUI
|
|
24
|
+
$ psql-explorer start the interactive TUI (also serves the web UI; --port picks its port)
|
|
24
25
|
$ psql-explorer envs
|
|
25
26
|
$ psql-explorer schemas --env <name>
|
|
26
27
|
$ psql-explorer tables --env <name> [--schema <schema>]
|
|
@@ -29,6 +30,7 @@ const cli = meow(`
|
|
|
29
30
|
$ psql-explorer query --env <name> "<sql>" (or "-", or pipe the SQL in)
|
|
30
31
|
$ psql-explorer info --env <name>
|
|
31
32
|
$ psql-explorer history --env <name>
|
|
33
|
+
$ psql-explorer diff --env <name> --with <name> [--schema <schema>] [--with-schema <schema>]
|
|
32
34
|
$ psql-explorer --web [--port 7878] [--no-open] serve the web UI and open it
|
|
33
35
|
$ psql-explorer web same as --web
|
|
34
36
|
$ psql-explorer configgen:file
|
|
@@ -46,6 +48,9 @@ const cli = meow(`
|
|
|
46
48
|
environment anything that would change data is refused by the server.
|
|
47
49
|
info Show the server version, database size and session settings.
|
|
48
50
|
history The statements run with query or the TUI, newest first.
|
|
51
|
+
diff Compare the structure of two environments' databases: tables,
|
|
52
|
+
columns, indexes, constraints, triggers, policies, views, functions,
|
|
53
|
+
sequences, types and extensions. All schemas, or the one --schema names.
|
|
49
54
|
web Serve the web UI and its API on http://127.0.0.1:<port> until stopped.
|
|
50
55
|
configgen:file Print a fully commented example environment kept in a directory.
|
|
51
56
|
configgen:vault Print the same for one kept in Vault.
|
|
@@ -57,7 +62,11 @@ const cli = meow(`
|
|
|
57
62
|
directory, or vault:<KV v2 path> such as vault:secret/data/psql-explorer.
|
|
58
63
|
It holds one <environment>/explorer.yaml per environment.
|
|
59
64
|
--env <name> The environment: a shared one, or a local connection by name
|
|
60
|
-
--schema <name> tables, describe, rows: the schema (default: the environment's)
|
|
65
|
+
--schema <name> tables, describe, rows: the schema (default: the environment's);
|
|
66
|
+
diff: compare only this schema
|
|
67
|
+
--with <name> diff: the environment to compare --env with
|
|
68
|
+
--with-schema <name>
|
|
69
|
+
diff: the schema on the --with side, when it is named differently
|
|
61
70
|
--format <f> table (default), csv or json
|
|
62
71
|
--json Same as --format json
|
|
63
72
|
--limit <n> rows: rows per page (default 100); history: how many (default 50)
|
|
@@ -71,6 +80,7 @@ const cli = meow(`
|
|
|
71
80
|
--port <n> web: the port to listen on (default 7878; the next free one is used if taken)
|
|
72
81
|
--help Show this help
|
|
73
82
|
--version Show the version
|
|
83
|
+
--relnotes Show the release notes
|
|
74
84
|
|
|
75
85
|
Run without a command to start the interactive TUI.
|
|
76
86
|
`, {
|
|
@@ -78,6 +88,8 @@ const cli = meow(`
|
|
|
78
88
|
flags: {
|
|
79
89
|
config: { type: 'string' },
|
|
80
90
|
env: { type: 'string' },
|
|
91
|
+
with: { type: 'string' },
|
|
92
|
+
withSchema: { type: 'string' },
|
|
81
93
|
schema: { type: 'string' },
|
|
82
94
|
format: { type: 'string' },
|
|
83
95
|
json: { type: 'boolean' },
|
|
@@ -89,9 +101,21 @@ const cli = meow(`
|
|
|
89
101
|
timeout: { type: 'number' },
|
|
90
102
|
port: { type: 'number' },
|
|
91
103
|
web: { type: 'boolean' },
|
|
104
|
+
relnotes: { type: 'boolean' },
|
|
92
105
|
open: { type: 'boolean', default: true },
|
|
93
106
|
},
|
|
94
107
|
});
|
|
108
|
+
// RELEASES.md ships next to dist, like web/dist, and needs neither the database nor a config.
|
|
109
|
+
if (cli.flags.relnotes) {
|
|
110
|
+
try {
|
|
111
|
+
process.stdout.write(readFileSync(join(import.meta.dirname, '..', 'RELEASES.md'), 'utf8'));
|
|
112
|
+
process.exit(0);
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
process.stderr.write('psql-explorer: the release notes are missing from this install\n');
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
95
119
|
// Prints an example config and needs neither the local database nor a config.
|
|
96
120
|
const [command, ...args] = cli.input;
|
|
97
121
|
if (command === 'configgen:file' || command === 'configgen:vault') {
|
|
@@ -163,7 +187,6 @@ if (command === 'web' || (command === undefined && cli.flags.web)) {
|
|
|
163
187
|
// Questions are answered in the browser: the page polls /api/prompt and posts the answer.
|
|
164
188
|
resolvers: defaultRegistry(undefined, prompts, store, vault),
|
|
165
189
|
prompts,
|
|
166
|
-
groups: vault.groups,
|
|
167
190
|
openSession,
|
|
168
191
|
};
|
|
169
192
|
const server = createWebServer({
|
|
@@ -188,10 +211,8 @@ if (command === 'web' || (command === undefined && cli.flags.web)) {
|
|
|
188
211
|
}
|
|
189
212
|
process.once('SIGINT', () => {
|
|
190
213
|
server.close();
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
process.exit(0);
|
|
194
|
-
});
|
|
214
|
+
store.close();
|
|
215
|
+
process.exit(0);
|
|
195
216
|
});
|
|
196
217
|
await new Promise(() => { });
|
|
197
218
|
}
|
|
@@ -212,7 +233,6 @@ if (command !== undefined) {
|
|
|
212
233
|
config,
|
|
213
234
|
configError,
|
|
214
235
|
resolvers: defaultRegistry(undefined, prompter, store, vault),
|
|
215
|
-
groups: vault.groups,
|
|
216
236
|
openSession,
|
|
217
237
|
};
|
|
218
238
|
code = await runCommand({
|
|
@@ -226,8 +246,6 @@ if (command !== undefined) {
|
|
|
226
246
|
stdinIsTTY: process.stdin.isTTY === true,
|
|
227
247
|
});
|
|
228
248
|
}
|
|
229
|
-
// Credentials and certificates issued for this command end with it.
|
|
230
|
-
await vault.groups.releaseAll();
|
|
231
249
|
store.close();
|
|
232
250
|
// Let piped output drain before exiting.
|
|
233
251
|
await new Promise((resolve) => process.stdout.write('', resolve));
|
|
@@ -249,15 +267,41 @@ const services = {
|
|
|
249
267
|
store,
|
|
250
268
|
tester: testConnection,
|
|
251
269
|
resolvers: defaultRegistry(undefined, prompts, store, vault),
|
|
252
|
-
groups: vault.groups,
|
|
253
270
|
prompts,
|
|
254
271
|
openSession,
|
|
272
|
+
openUrl: openBrowser,
|
|
255
273
|
};
|
|
256
274
|
services.startup = createStartup({ services, base: cli.flags.config, vault });
|
|
275
|
+
// The web UI is served from the same process, so the Environments screen can say where it is.
|
|
276
|
+
// Questions it triggers are answered in the browser, so it gets a prompt queue of its own; the
|
|
277
|
+
// config is the one the splash screen loads. Failing to start it never stops the TUI.
|
|
278
|
+
const webPrompts = createPromptQueue();
|
|
279
|
+
const webServices = {
|
|
280
|
+
store,
|
|
281
|
+
tester: testConnection,
|
|
282
|
+
resolvers: defaultRegistry(undefined, webPrompts, store, vault),
|
|
283
|
+
prompts: webPrompts,
|
|
284
|
+
openSession,
|
|
285
|
+
get config() {
|
|
286
|
+
return services.config;
|
|
287
|
+
},
|
|
288
|
+
get configError() {
|
|
289
|
+
return services.configError;
|
|
290
|
+
},
|
|
291
|
+
};
|
|
292
|
+
const webServer = createWebServer({
|
|
293
|
+
services: webServices,
|
|
294
|
+
staticDir: join(import.meta.dirname, '..', 'web', 'dist'),
|
|
295
|
+
});
|
|
296
|
+
try {
|
|
297
|
+
services.webUrl = await listen(webServer, cli.flags.port ?? DEFAULT_WEB_PORT);
|
|
298
|
+
}
|
|
299
|
+
catch {
|
|
300
|
+
webServer.close();
|
|
301
|
+
}
|
|
257
302
|
const app = render(_jsx(App, { version: cli.pkg.version ?? '0.0.0', fullScreen: true, services: services }),
|
|
258
303
|
// The app owns the whole terminal, and the shell's screen comes back when it exits.
|
|
259
304
|
{ alternateScreen: true });
|
|
260
305
|
await app.waitUntilExit();
|
|
261
|
-
|
|
262
|
-
await vault.groups.releaseAll();
|
|
306
|
+
webServer.close();
|
|
263
307
|
store.close();
|
|
@@ -12,6 +12,19 @@ protected: true
|
|
|
12
12
|
# Open every session read only (default_transaction_read_only), so nothing run here can change
|
|
13
13
|
# data. Optional, default: the value of protected.
|
|
14
14
|
readOnly: true
|
|
15
|
+
|
|
16
|
+
# Opening the environment first asks for two random words to be typed back, so it is never
|
|
17
|
+
# entered by accident. Optional, default: the value of protected.
|
|
18
|
+
confirm: true
|
|
19
|
+
|
|
20
|
+
# Offer the web UI's Seed page, which fills tables with generated rows. Optional, default: true
|
|
21
|
+
# for the development and local tiers, false otherwise.
|
|
22
|
+
# seeder: false
|
|
23
|
+
|
|
24
|
+
# Let the web UI's Design page run statements that create, alter and drop tables; without it the
|
|
25
|
+
# page only previews the SQL. Optional, default: true for the development and local tiers, false
|
|
26
|
+
# otherwise.
|
|
27
|
+
# designer: false
|
|
15
28
|
`;
|
|
16
29
|
const fileEnvironment = `# One environment: <environment>/explorer.yaml below the base path.
|
|
17
30
|
# Use with: psql-explorer --config <base path> (or export PSQL_EXPLORER_ENVIRONMENTS=<base path>)
|
|
@@ -28,13 +41,13 @@ database: app
|
|
|
28
41
|
# The schema browsing starts in. Optional, default: public
|
|
29
42
|
# schema: app
|
|
30
43
|
|
|
31
|
-
# Database login.
|
|
44
|
+
# Database login. Required.
|
|
32
45
|
username: readonly
|
|
33
46
|
|
|
34
47
|
# password (default) needs a password; none means the database takes no password.
|
|
35
48
|
auth: password
|
|
36
49
|
|
|
37
|
-
# Required when auth is password.
|
|
50
|
+
# Required when auth is password. Must be left out when auth is none.
|
|
38
51
|
password:
|
|
39
52
|
source: file
|
|
40
53
|
value: ./password.txt # relative to this file's folder; the whole content, trimmed
|
|
@@ -85,34 +98,21 @@ database: app
|
|
|
85
98
|
# The schema browsing starts in. Optional, default: public
|
|
86
99
|
# schema: app
|
|
87
100
|
|
|
88
|
-
|
|
89
|
-
#
|
|
90
|
-
# password
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
value: database/creds/readonly
|
|
94
|
-
# Or a fixed login instead of credentials:
|
|
95
|
-
# username: readonly
|
|
96
|
-
# auth: password # password (default) | none
|
|
97
|
-
# password: # required when auth is password
|
|
98
|
-
# source: vault
|
|
99
|
-
# value: secret/data/db/prod#password
|
|
101
|
+
username: readonly
|
|
102
|
+
auth: password # password (default) | none; none takes no password, so leave password out
|
|
103
|
+
password: # required when auth is password
|
|
104
|
+
source: vault
|
|
105
|
+
value: secret/data/db/prod#password
|
|
100
106
|
|
|
101
107
|
# TLS. Optional, default: mode disable
|
|
102
108
|
tls:
|
|
103
109
|
# disable | require | verify-ca | verify-full
|
|
104
110
|
mode: verify-full
|
|
105
|
-
#
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
commonName: psql-explorer
|
|
111
|
-
# CA certificate (PEM text). Required for verify-ca and verify-full unless client is given.
|
|
112
|
-
# ca:
|
|
113
|
-
# source: vault
|
|
114
|
-
# value: secret/data/db/prod#ca
|
|
115
|
-
# A fixed client certificate and key instead of client. Give both or neither.
|
|
111
|
+
# CA certificate (PEM text). Required for verify-ca and verify-full.
|
|
112
|
+
ca:
|
|
113
|
+
source: vault
|
|
114
|
+
value: secret/data/db/prod#ca
|
|
115
|
+
# Client certificate and key, for mutual TLS. Give both or neither.
|
|
116
116
|
# cert:
|
|
117
117
|
# source: vault
|
|
118
118
|
# value: secret/data/db/prod#cert
|