@mason-data/cli 0.0.0-stage → 0.7.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 +202 -0
- package/README.md +268 -2
- package/dist/src/api/error.js +18 -0
- package/dist/src/api/postgres/catalog-worker.js +92 -0
- package/dist/src/api/postgres/catalog.js +477 -0
- package/dist/src/api/postgres/encode.js +541 -0
- package/dist/src/api/postgres/explain.js +143 -0
- package/dist/src/api/postgres/listener.js +60 -0
- package/dist/src/api/postgres/parser-worker.js +58 -0
- package/dist/src/api/postgres/parser.js +121 -0
- package/dist/src/api/postgres/plain.js +33 -0
- package/dist/src/api/postgres/protocol.js +404 -0
- package/dist/src/api/postgres/schema.js +54 -0
- package/dist/src/api/postgres/server.js +1771 -0
- package/dist/src/api/postgres/translate.js +248 -0
- package/dist/src/api/request-log.js +50 -0
- package/dist/src/cdc/pg-wire.js +588 -0
- package/dist/src/cdc/pgoutput.js +107 -0
- package/dist/src/cdc/poll.js +67 -0
- package/dist/src/cdc/source.js +65 -0
- package/dist/src/cli/background.js +198 -0
- package/dist/src/cli/command.js +173 -0
- package/dist/src/cli/commands.js +1277 -0
- package/dist/src/cli/control.js +36 -0
- package/dist/src/cli/demo.js +149 -0
- package/dist/src/cli/dev.js +669 -0
- package/dist/src/cli/doctor.js +260 -0
- package/dist/src/cli/guides.js +158 -0
- package/dist/src/cli/init.js +366 -0
- package/dist/src/cli/machine.js +162 -0
- package/dist/src/cli/main.js +83 -0
- package/dist/src/cli/mcp.js +726 -0
- package/dist/src/cli/pg-tls.js +100 -0
- package/dist/src/cli/pg-url.js +52 -0
- package/dist/src/cli/pin.js +36 -0
- package/dist/src/cli/ports.js +118 -0
- package/dist/src/cli/preflight.js +347 -0
- package/dist/src/cli/project.js +129 -0
- package/dist/src/cli/readiness.js +76 -0
- package/dist/src/cli/registry.js +220 -0
- package/dist/src/cli/replication-checks.js +120 -0
- package/dist/src/cli/replication-mode.js +148 -0
- package/dist/src/cli/run-server.js +311 -0
- package/dist/src/cli/runs.js +120 -0
- package/dist/src/cli/security.js +86 -0
- package/dist/src/cli/self-signed-cert.js +64 -0
- package/dist/src/cli/serve.js +301 -0
- package/dist/src/cli/source-edit.js +116 -0
- package/dist/src/cli/sources.js +564 -0
- package/dist/src/cli/state.js +58 -0
- package/dist/src/cli/style.js +187 -0
- package/dist/src/cli/supervise.js +248 -0
- package/dist/src/cli/sync.js +94 -0
- package/dist/src/cli/ui.js +107 -0
- package/dist/src/cli/user.js +229 -0
- package/dist/src/cli/version.js +23 -0
- package/dist/src/demo/seed.js +237 -0
- package/dist/src/demo/source.js +655 -0
- package/dist/src/duck/analyze.js +113 -0
- package/dist/src/duck/cancel.js +49 -0
- package/dist/src/duck/distinct-key.js +228 -0
- package/dist/src/duck/executor-worker.js +268 -0
- package/dist/src/duck/executor.js +567 -0
- package/dist/src/duck/explain.js +238 -0
- package/dist/src/duck/functions.js +819 -0
- package/dist/src/duck/gaps.js +94 -0
- package/dist/src/duck/jsonb-order.js +175 -0
- package/dist/src/duck/jsonb.js +188 -0
- package/dist/src/duck/kept-route.js +299 -0
- package/dist/src/duck/kept.js +964 -0
- package/dist/src/duck/late.js +221 -0
- package/dist/src/duck/libc-order.js +190 -0
- package/dist/src/duck/pg-eval-worker.js +34 -0
- package/dist/src/duck/pg-eval.js +59 -0
- package/dist/src/duck/pgtree.js +378 -0
- package/dist/src/duck/plan.js +259 -0
- package/dist/src/duck/publication.js +207 -0
- package/dist/src/duck/requests.js +209 -0
- package/dist/src/duck/resolved.js +248 -0
- package/dist/src/duck/rewrite.js +5101 -0
- package/dist/src/duck/store.js +907 -0
- package/dist/src/duck/stream.js +518 -0
- package/dist/src/duck/sync.js +2929 -0
- package/dist/src/duck/to-char.js +482 -0
- package/dist/src/duck/token.js +31 -0
- package/dist/src/duck/types.js +427 -0
- package/dist/src/duck/users.js +231 -0
- package/dist/src/duck/verified.js +1001 -0
- package/dist/src/duck/views.js +150 -0
- package/dist/src/lib/database-url.js +74 -0
- package/dist/src/lib/did-you-mean.js +43 -0
- package/dist/src/lib/dotenv.js +72 -0
- package/dist/src/lib/exit-when-written.js +20 -0
- package/dist/src/lib/float-equal.js +69 -0
- package/dist/src/lib/http-json.js +22 -0
- package/dist/src/lib/log.js +161 -0
- package/dist/src/lib/pg-numeric.js +489 -0
- package/dist/src/lib/port.js +22 -0
- package/dist/src/lib/project-file.js +49 -0
- package/dist/src/lib/project-files.js +43 -0
- package/dist/src/lib/project-version.js +76 -0
- package/dist/src/lib/runnable.js +14 -0
- package/dist/src/lib/shell-word.js +4 -0
- package/dist/src/lib/timezone.js +237 -0
- package/dist/src/lib/zone-names.js +114 -0
- package/dist/src/web/app.css +76 -0
- package/dist/src/web/app.js +491 -0
- package/dist/src/web/index.html +44 -0
- package/package.json +22 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
+
exercising permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
36
|
+
Object form, made available under the License, as indicated by a
|
|
37
|
+
copyright notice that is included in or attached to the work
|
|
38
|
+
(an example is provided in the Appendix below).
|
|
39
|
+
|
|
40
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
41
|
+
form, that is based on (or derived from) the Work and for which the
|
|
42
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
43
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
44
|
+
of this License, Derivative Works shall not include works that remain
|
|
45
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
46
|
+
the Work and Derivative Works thereof.
|
|
47
|
+
|
|
48
|
+
"Contribution" shall mean any work of authorship, including
|
|
49
|
+
the original version of the Work and any modifications or additions
|
|
50
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
51
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
52
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
53
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
54
|
+
means any form of electronic, verbal, or written communication sent
|
|
55
|
+
to the Licensor or its representatives, including but not limited to
|
|
56
|
+
communication on electronic mailing lists, source code control systems,
|
|
57
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
58
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
59
|
+
excluding communication that is conspicuously marked or otherwise
|
|
60
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
61
|
+
|
|
62
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
63
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
64
|
+
subsequently incorporated within the Work.
|
|
65
|
+
|
|
66
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
67
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
68
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
69
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
70
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
71
|
+
Work and such Derivative Works in Source or Object form.
|
|
72
|
+
|
|
73
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
74
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
75
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
76
|
+
(except as stated in this section) patent license to make, have made,
|
|
77
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
78
|
+
where such license applies only to those patent claims licensable
|
|
79
|
+
by such Contributor that are necessarily infringed by their
|
|
80
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
81
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
82
|
+
institute patent litigation against any entity (including a
|
|
83
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
84
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
85
|
+
or contributory patent infringement, then any patent licenses
|
|
86
|
+
granted to You under this License for that Work shall terminate
|
|
87
|
+
as of the date such litigation is filed.
|
|
88
|
+
|
|
89
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
90
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
91
|
+
modifications, and in Source or Object form, provided that You
|
|
92
|
+
meet the following conditions:
|
|
93
|
+
|
|
94
|
+
(a) You must give any other recipients of the Work or
|
|
95
|
+
Derivative Works a copy of this License; and
|
|
96
|
+
|
|
97
|
+
(b) You must cause any modified files to carry prominent notices
|
|
98
|
+
stating that You changed the files; and
|
|
99
|
+
|
|
100
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
101
|
+
that You distribute, all copyright, patent, trademark, and
|
|
102
|
+
attribution notices from the Source form of the Work,
|
|
103
|
+
excluding those notices that do not pertain to any part of
|
|
104
|
+
the Derivative Works; and
|
|
105
|
+
|
|
106
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
107
|
+
distribution, then any Derivative Works that You distribute must
|
|
108
|
+
include a readable copy of the attribution notices contained
|
|
109
|
+
within such NOTICE file, excluding those notices that do not
|
|
110
|
+
pertain to any part of the Derivative Works, in at least one
|
|
111
|
+
of the following places: within a NOTICE text file distributed
|
|
112
|
+
as part of the Derivative Works; within the Source form or
|
|
113
|
+
documentation, if provided along with the Derivative Works; or,
|
|
114
|
+
within a display generated by the Derivative Works, if and
|
|
115
|
+
wherever such third-party notices normally appear. The contents
|
|
116
|
+
of the NOTICE file are for informational purposes only and
|
|
117
|
+
do not modify the License. You may add Your own attribution
|
|
118
|
+
notices within Derivative Works that You distribute, alongside
|
|
119
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
120
|
+
that such additional attribution notices cannot be construed
|
|
121
|
+
as modifying the License.
|
|
122
|
+
|
|
123
|
+
You may add Your own copyright statement to Your modifications and
|
|
124
|
+
may provide additional or different license terms and conditions
|
|
125
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
126
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
127
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
128
|
+
the conditions stated in this License.
|
|
129
|
+
|
|
130
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
131
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
132
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
133
|
+
this License, without any additional terms or conditions.
|
|
134
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
135
|
+
the terms of any separate license agreement you may have executed
|
|
136
|
+
with Licensor regarding such Contributions.
|
|
137
|
+
|
|
138
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
139
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
140
|
+
except as required for reasonable and customary use in describing the
|
|
141
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
142
|
+
|
|
143
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
144
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
145
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
146
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
147
|
+
implied, including, without limitation, any warranties or conditions
|
|
148
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
149
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
150
|
+
appropriateness of using or redistributing the Work and assume any
|
|
151
|
+
risks associated with Your exercise of permissions under this License.
|
|
152
|
+
|
|
153
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
154
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
155
|
+
unless required by applicable law (such as deliberate and grossly
|
|
156
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
157
|
+
liable to You for damages, including any direct, indirect, special,
|
|
158
|
+
incidental, or consequential damages of any character arising as a
|
|
159
|
+
result of this License or out of the use or inability to use the
|
|
160
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
161
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
162
|
+
other commercial damages or losses), even if such Contributor
|
|
163
|
+
has been advised of the possibility of such damages.
|
|
164
|
+
|
|
165
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
166
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
167
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
168
|
+
or other liability obligations and/or rights consistent with this
|
|
169
|
+
License. However, in accepting such obligations, You may act only
|
|
170
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
171
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
172
|
+
defend, and hold each Contributor harmless for any liability
|
|
173
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
174
|
+
of your accepting any such warranty or additional liability.
|
|
175
|
+
|
|
176
|
+
END OF TERMS AND CONDITIONS
|
|
177
|
+
|
|
178
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
179
|
+
|
|
180
|
+
To apply the Apache License to your work, attach the following
|
|
181
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
182
|
+
replaced with your own identifying information. (Don't include
|
|
183
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
184
|
+
comment syntax for the file format. We also recommend that a
|
|
185
|
+
file or class name and description of purpose be included on the
|
|
186
|
+
same "printed page" as the copyright notice for easier
|
|
187
|
+
identification within third-party archives.
|
|
188
|
+
|
|
189
|
+
Copyright [yyyy] [name of copyright owner]
|
|
190
|
+
|
|
191
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
192
|
+
you may not use this file except in compliance with the License.
|
|
193
|
+
You may obtain a copy of the License at
|
|
194
|
+
|
|
195
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
196
|
+
|
|
197
|
+
Unless required by applicable law or agreed to in writing, software
|
|
198
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
199
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
200
|
+
See the License for the specific language governing permissions and
|
|
201
|
+
limitations under the License.
|
|
202
|
+
|
package/README.md
CHANGED
|
@@ -1,3 +1,269 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @mason-data/cli
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The `mason` command points [Mason](https://masondata.ai) at the Postgres your app already uses, copies the tables you
|
|
4
|
+
add, keeps the copy current, and serves it to BI tools and your app over the Postgres protocol. Every command works the
|
|
5
|
+
same for you and for a coding agent (Claude Code, Codex, …): it prints for a person, or with `--json` as data, and its
|
|
6
|
+
exit code says how it went ([for coding agents](#for-coding-agents)).
|
|
7
|
+
|
|
8
|
+
There is nothing to clone or build: install this package and run `mason`. It needs Node 22.13 or later and nothing
|
|
9
|
+
else — no Docker, no database of its own; `mason doctor` checks this machine and says what is missing. On a terminal
|
|
10
|
+
its output is coloured and wrapped to the window, and a command that takes a moment shows a spinner; piped,
|
|
11
|
+
redirected, with `--json` or with `NO_COLOR` set, it is plain text.
|
|
12
|
+
|
|
13
|
+
**A Mason project is your app's folder.** `mason init` makes it one: its sources file in `mason/sources/` — the
|
|
14
|
+
database and the tables to copy — yours to commit; this machine's copy of your data and Mason's state in `.mason/`,
|
|
15
|
+
both rebuilt from that file. Every other command works
|
|
16
|
+
anywhere in the folder, as git does. `mason destroy` removes the copy and the state. ([Cleaning up](#cleaning-up))
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
npm i -g @mason-data/cli
|
|
20
|
+
cd my-app
|
|
21
|
+
mason doctor # Node, memory, ports: what is missing, and what to do
|
|
22
|
+
mason init # this folder becomes a project, on the database in .env's DATABASE_URL (checked first)
|
|
23
|
+
mason tables # the tables: rows, keys, added; a table's columns with `mason tables orders`
|
|
24
|
+
mason add orders customers # add them to the sources file (--columns id,status: those only)
|
|
25
|
+
mason start # Mason in the background, copying them, then returns
|
|
26
|
+
mason wait # until every added table is copied and served
|
|
27
|
+
mason user add metabase # a user for a BI tool: its password, printed once, and the URL to paste
|
|
28
|
+
mason ui # the status page and the users, in your browser
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`mason init` takes the database's URL as an argument too (`mason init postgres://user:password@localhost:5432/your_app`);
|
|
32
|
+
without one it reads `DATABASE_URL` from the folder's `.env.local`, `.env` or the shell. The sources file it writes says
|
|
33
|
+
`url: ${DATABASE_URL}`, and the URL itself stays in `.env` (init writes the line there when you gave the URL as an
|
|
34
|
+
argument or it was only in the shell, and gitignores that file): change it there when the password changes. The
|
|
35
|
+
database is required.
|
|
36
|
+
|
|
37
|
+
**No database at hand?** `mason init demo && mason start`, in a new folder, makes a project on a demo database Mason
|
|
38
|
+
hosts itself — a small shop (`customers`, `products`, `orders`, `order_items`; the first three added, `order_items`
|
|
39
|
+
left for your first `mason add order_items` after `mason start`), kept in `.mason/demo/`, served on a port of this machine, with a new order every few seconds (`MASON_DEMO_CHANGE_MS`, default 5000) so
|
|
40
|
+
`mason status` shows the copy keeping up. Nothing to install. It is read by polling only (it has no change log), and
|
|
41
|
+
`mason destroy` removes it with the copy.
|
|
42
|
+
|
|
43
|
+
## The project: its sources file, in your app
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
my-app/
|
|
47
|
+
.env the database's URL (DATABASE_URL), as your app has it (gitignored)
|
|
48
|
+
.mason/ this machine's: Mason's key, the copy of your data, its state and logs (gitignored)
|
|
49
|
+
mason/sources/app.yml the database and the tables Mason copies — commit it
|
|
50
|
+
mason/mason.yml the Mason version the project is written for — commit it: production runs that version
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The sources file is named for the database (`app.yml` above) and lists what Mason copies, one entry per table:
|
|
54
|
+
|
|
55
|
+
```yaml
|
|
56
|
+
kind: postgres
|
|
57
|
+
url: ${DATABASE_URL} # a variable from .env.local, .env or the shell; a literal URL is allowed for local dev
|
|
58
|
+
tables:
|
|
59
|
+
orders: # a bare name is in `public`; `billing.invoices` names its schema
|
|
60
|
+
columns: [id, customer_id, status, amount, created_at, payload]
|
|
61
|
+
customers: {} # every column
|
|
62
|
+
events:
|
|
63
|
+
exclude: [raw_body] # every column but these
|
|
64
|
+
sessions:
|
|
65
|
+
key: [tenant_id, id] # only for a table with no primary key: the unique columns to copy it by
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
- An entry is `{}` (every column), `columns:` (those; the primary key is always copied), `exclude:` (every column but
|
|
69
|
+
those) — never both — and, for a table with no primary key, `key:`: the columns that tell its rows apart, which it is
|
|
70
|
+
copied by (polling only: logical replication cannot apply a change by them). A table with a primary key refuses `key:`.
|
|
71
|
+
- Column types are not in the file: Mason reads them from the database at every start.
|
|
72
|
+
- `url:` is `${NAME}`, read from `.env.local`, `.env` or the shell — or a literal URL, for a database on your machine.
|
|
73
|
+
- One source per project for now, and `postgres` is its only kind.
|
|
74
|
+
- `mason add` and `mason remove` edit one entry and keep every other byte of the file, your comments included, and
|
|
75
|
+
apply at once; an entry you edit by hand is applied at the next `mason start`. A mistake — a column the table does
|
|
76
|
+
not have, `columns` and `exclude` together, a variable that is not set — is refused, every one of the file's
|
|
77
|
+
mistakes on one report, each with its key and the way out (`did you mean …?`).
|
|
78
|
+
|
|
79
|
+
`mason init --folder <dir>` puts `sources/` elsewhere in the folder (`--folder .` for the folder itself). Keep it in
|
|
80
|
+
your app's repo, so a schema change and the tables that copy it travel together. A teammate who clones the repo runs
|
|
81
|
+
`mason init` once: the sources file is kept, and `.mason/` is per machine.
|
|
82
|
+
|
|
83
|
+
**A project an older Mason made** (its tables listed in `mason/models/`) is refused by `mason start`; nothing is
|
|
84
|
+
migrated. `mason init` in it writes the sources file alone — its `.mason/` and copy kept — then `mason add` each table
|
|
85
|
+
again and remove `mason/models/`.
|
|
86
|
+
|
|
87
|
+
## Commands
|
|
88
|
+
|
|
89
|
+
| Command | What it does |
|
|
90
|
+
|---|---|
|
|
91
|
+
| `mason help [<command> \| <guide>]` | The commands by step, one command's usage, or one of the guides, offline: `mason help agent` (driving Mason from a coding agent: the commands in order, `--json` and its shapes, exit codes, failures) and `mason help production` (the road from a laptop to production: the image, its settings, Docker Compose, logical replication, upgrades). |
|
|
92
|
+
| `mason doctor` | Checks what Mason needs here — Node 22.13+, memory, Mason's ports — and in a project's folder its database; with Mason running for it, the run itself: whether every added table is copied, the statements refused or slow in the last day. Every failing line says what to do; it ends with the next command. |
|
|
93
|
+
| `mason init [<postgres url>]` | Makes this folder a project: checks it can read the database (nothing is written when it cannot; `--skip-check` writes anyway), writes `.mason/config.json` (Mason's key, never the URL) and `mason/sources/<database>.yml` (keeping a sources file already there), adds the URL to `.env` when it was not there, and adds `.mason/` and that `.env` to `.gitignore`. The URL from the argument, else `DATABASE_URL` in `.env.local`, `.env` or the shell. `--name` sets the project's name (default: the folder's) — the database name a tool connects to — `--folder` where its files go, `--remote` allows a database on another machine. Refuses a folder already in a project, but for one with no sources file (an older Mason's), whose sources file alone it writes. |
|
|
94
|
+
| `mason start` | Starts Mason for this folder's project in the background and returns once it is up. Run it again any time: it says so when Mason is already running. `--replication` reads the database's change log as production does (below); `--poll` goes back. `--self-signed-tls` serves the Postgres API over TLS; `--attach` stays in the foreground. |
|
|
95
|
+
| `mason tables [<table>…] [--all]` | The tables this user can read against the sources file, one line each: rows, primary key, whether added (and with which columns), and why a table cannot be added yet (no primary key: the `--key` to add it by). Name a table (or pass `--all` for every one) for its columns: each one's type, what it references, and whether `mason add` would leave it out as a secret. It reads the project's database itself, so Mason need not be running. |
|
|
96
|
+
| `mason add <table>… [--columns …] [--include …] [--key …]` | Copies tables into Mason, each one an entry of the sources file: every column (`orders: {}`), or `--columns id,status,created_at` for those (one table at a time; the key is written with them). Columns that look like secrets (`password`, `token`, `api_key`, `private_key`, …) are left out, named, and written as `exclude:`; `--include password_hash` adds one. `--key tenant_id,id` adds a table with no primary key, copied by those columns. Adding a table again with other columns changes its entry. Mason need not be running: the entry is checked against the database as the run checks it, and the copy begins at `mason start` (with Mason running, at once). |
|
|
97
|
+
| `mason remove <table>…` | Takes tables out of the sources file — each one's entry, every other byte as it was — and Mason drops their copies: at once when it is running, else at the next `mason start`. Mason need not be running. |
|
|
98
|
+
| `mason wait [--timeout <s>]` | Waits until every added table is copied and served, showing every added table on a line of its own: 100% for the finished ones, in the order they finished, and a bar for each still copying (rows, %, rate, time left) at the bottom. Exit 0 ready, 1 a copy failed (printed, with what to do), 124 timed out (default 600 s). |
|
|
99
|
+
| `mason user add\|list\|remove <name>` | The users of the Postgres API: a name and a password that does not expire, as a BI tool's datasource form asks for them or an app's `DATABASE_URL` carries them. `add` prints the password once with the URL; `remove` refuses the next connection at once and ends the sessions it holds at their next statement, without a restart. |
|
|
100
|
+
| `mason mcp [--url <url>]` | An MCP server on stdio for Claude Code, Claude Desktop and other agents (below): the tables Mason serves with each copy's rows and how current it is, a table's columns, a statement's rows with their Postgres types or Mason's refusal with its way out, and EXPLAIN — connected as a `mason user` (`MASON_URL`, else `--url`), beside `mason start` or a `mason serve` elsewhere. |
|
|
101
|
+
| `mason ui` | Opens the running Mason's web page in your browser: what `mason status` says, refreshed every 2 s, and the users of `mason user` (add one, its password shown once; remove one). It prints the URL with the run's token, which the page trades for a cookie on the first visit; `--no-open` prints it only, `--json` is `{ url, port }`. `mason start`'s alone: `mason serve` serves no page. |
|
|
102
|
+
| `mason status` | What Mason is doing: the database and how its changes are read, the project folder, the copy's file, the Postgres API and how it listens, each table's copy, the statements answered slowly in the last day and the ones refused or failed, each with its reason. In replication mode, where the stream is. |
|
|
103
|
+
| `mason sync <table>…` | Makes the copy of these tables current now: a table over the sync's row limit (1,000,000 rows) is copied and served but kept current only so, or by replication. |
|
|
104
|
+
| `mason logs [-n <lines>] [-f] [--start] [--requests [--failed]]` | Mason's log: every copy, poll and statement, with warnings and errors and their reasons; `-f` follows it, `--start` shows what `mason start` printed instead. `--requests` is the request log: every statement a tool sent, when, the outcome and SQLSTATE, how long, the rows, the tool and the user; `--failed` only the refused and failed ones. |
|
|
105
|
+
| `mason stop` | Stops Mason for this project. The copy and the state stay: the next `mason start` resumes in seconds. |
|
|
106
|
+
| `mason destroy` | Removes this project's copy of the data (its file) and Mason's state in `.mason/`, stopping Mason first if it runs — and, for a project started with `--replication`, its slot and publication on the database. Your files, `.mason/config.json` and the users stay: the next `mason start` copies the tables again. |
|
|
107
|
+
| `mason serve` | Mason for a server or a container: `mason start --attach` configured by the environment alone, its state in one folder (a volume), its log on stdout, `GET /healthz`, a clean stop on SIGTERM. `mason serve --help` lists every setting with its default. |
|
|
108
|
+
|
|
109
|
+
Every command documents itself: `mason --help` lists them, and `mason <command> --help` (or `mason help <command>`) says
|
|
110
|
+
how to use one, with examples. Every command but `help` and `mcp` (whose stdout is the MCP protocol's) takes `--json`. A command means the project of the folder it
|
|
111
|
+
runs in, or the nearest one above it.
|
|
112
|
+
|
|
113
|
+
## What `mason start` does
|
|
114
|
+
|
|
115
|
+
1. **It only reads the database** — the one the sources file names. Mason polls it with `SELECT` statements. It does not
|
|
116
|
+
need `wal_level = logical`, a replication slot, a publication or the `REPLICATION` attribute, and it creates none of
|
|
117
|
+
them. When you stop it, nothing is left behind on the database. A table over 1,000,000 rows is copied and served but
|
|
118
|
+
kept current only by `mason sync` or by replication, and `mason status` says so on its line. (`--replication` is the
|
|
119
|
+
exception, said below: it reads the change log, as production does, and makes its own slot.)
|
|
120
|
+
2. **It copies each added table** into a DuckDB file in `.mason/`, a range of pages at a time, a few in parallel, one
|
|
121
|
+
Postgres backend each, smallest table first; a copy that stops continues with the ranges it did not commit. The table
|
|
122
|
+
is served the moment its copy lands; the copy runs beside the polls, never instead of them.
|
|
123
|
+
3. **It serves the Postgres API** on 5433, or on the next port free on every address when something holds 5433 (often a
|
|
124
|
+
second Postgres; `mason start` says what held it) — and records it: every later start listens on the same port, so a
|
|
125
|
+
tool's saved connection keeps working, and refuses, naming what holds it, rather than move. `MASON_PG_PORT=55433` in
|
|
126
|
+
your shell names the port, or moves the project to it (said). It listens on 127.0.0.1 unless `MASON_PG_HOST` names
|
|
127
|
+
another address; off this machine it needs TLS (`--self-signed-tls`, or `MASON_PG_TLS_CERT` / `MASON_PG_TLS_KEY`).
|
|
128
|
+
4. **It runs in the background**, copying what the sources file names: `mason add` and `mason remove` change it at once,
|
|
129
|
+
a hand edit (or an add or remove made while Mason was stopped) at the next `mason start`.
|
|
130
|
+
|
|
131
|
+
Mason's own settings (`MASON_POLL_MS`, `MASON_DUCKDB_MEMORY`, `MASON_COPY_PARALLEL`, …) set in your shell reach Mason.
|
|
132
|
+
A copy reads `MASON_COPY_PARALLEL` chunks at once (default 3), each sized to take about `MASON_COPY_CHUNK_SECONDS` to
|
|
133
|
+
read (default 12) at the rate it measures on your connection, so `mason wait` moves every few seconds even over a slow
|
|
134
|
+
link, and a copy that stops reads at most those chunks again.
|
|
135
|
+
A JSON value your queries keep reading (`payload ->> 'customer_id'`) is stored as a column of the copy and read from
|
|
136
|
+
there (`mason status` lists them); `MASON_STORE_JSON_PATHS=off` turns that off.
|
|
137
|
+
|
|
138
|
+
## `mason start` options
|
|
139
|
+
|
|
140
|
+
| Option | What it does |
|
|
141
|
+
|---|---|
|
|
142
|
+
| `--replication` | Read the database's changes from its change log (logical replication), as production does, instead of polling. It needs `wal_level = logical` and a user that may replicate, and makes its own publication and slot (`mason_dev_<project>_<source>`, the source the sources file's name; past 63 bytes, a prefix and a hash) on the database. The project keeps the mode until `--poll`; a switch keeps the copies and catches each table up by one poll. `mason stop` leaves the slot (it holds WAL until the next start); `mason destroy` and `--poll` drop it. |
|
|
143
|
+
| `--publication <name>` | With `--replication`: a publication the database owner made (else `PG_PUBLICATION`), read as it is, verified to carry every added table and column, never created, altered or dropped. A read replica needs one. |
|
|
144
|
+
| `--poll` | Read them with `SELECT` again (the default). |
|
|
145
|
+
| `--self-signed-tls` | The Postgres API requires TLS, from a certificate made for this project in `.mason/` (served again by every later start with the flag); clients connect with `sslmode=require`. |
|
|
146
|
+
| `--port <n>` | The port the `mason` commands reach it on (default 4039, or the next free one). |
|
|
147
|
+
| `--attach` | Stay in the foreground: the terminal shows Mason's progress, and Ctrl-C stops it. |
|
|
148
|
+
|
|
149
|
+
A database on another machine needs `mason init --remote`, which records that you mean it: the copy leaves the
|
|
150
|
+
database's network, and each added table under the row limit is read whole every few seconds (`--replication` reads only
|
|
151
|
+
its changes). Point it at a read replica, never the primary your app writes to.
|
|
152
|
+
|
|
153
|
+
**When the database moves.** Mason records the Postgres server it copied from. The same database on a new server (a
|
|
154
|
+
restore, a move to another host) is read again at the next `mason start`: every row is compared with the copy, and only
|
|
155
|
+
what changed is written. A different database is refused: `mason destroy`, then `mason start`, copies it.
|
|
156
|
+
|
|
157
|
+
## For coding agents
|
|
158
|
+
|
|
159
|
+
`mason help agent` is the page an agent reads first, offline: the commands in the order a project takes them, every
|
|
160
|
+
answer's shape, the exit codes and the failure codes. In short:
|
|
161
|
+
|
|
162
|
+
- Every command but `help` and `mcp` takes `--json` and answers with **one JSON object on stdout**, always with
|
|
163
|
+
`next: [{ command, what }]` — the commands to run next, the same a person reads under `Next:`. The other keys are the
|
|
164
|
+
command's own; `mason help <command>` names them on its `--json` line. Lists are wrapped (`{ tables: […], next }`).
|
|
165
|
+
- A failure is `{ "error": { "code", "message", "why", "fix" }, "next": […] }` on stdout, and the **exit code** says it:
|
|
166
|
+
0 done, 1 failed with the reason, 2 the command line is wrong, 124 `mason wait` ran out of time. The codes are a closed
|
|
167
|
+
set (`usage`, `no_project`, `bad_config`, `bad_source`, `not_running`, `unreachable`, `not_found`, `nothing_yet`,
|
|
168
|
+
`refused`, `failed`, `timeout`). For a person the same failure is three short lines — what is wrong, `why`, `fix` — then `Next:`.
|
|
169
|
+
- Progress (a spinner, `mason wait`'s bars) goes to stderr, and only on a terminal; a pipe gets plain text, and nothing
|
|
170
|
+
under `--json`. The Postgres API's URL is in `mason status --json` (`postgres.url`); a user's password is printed by
|
|
171
|
+
`mason user add` once: never read `.mason/config.json`.
|
|
172
|
+
|
|
173
|
+
```sh
|
|
174
|
+
mason doctor --json && mason init --json && mason start --json
|
|
175
|
+
mason tables --json # choose the tables
|
|
176
|
+
mason add orders customers --json && mason wait --json
|
|
177
|
+
mason user add app --json # the password and URL for the app, as data
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### From an MCP client: `mason mcp`
|
|
181
|
+
|
|
182
|
+
A chat agent asks Mason with tools, over MCP, rather than through a shell. `mason mcp` is an MCP server on stdin and
|
|
183
|
+
stdout (newline-delimited JSON-RPC; stdout carries the protocol alone, what it says goes to stderr) that serves until
|
|
184
|
+
the client closes stdin; it takes no `--json`, and says a failure to start on stderr. It connects to the Postgres API as
|
|
185
|
+
a `mason user`, by the URL `mason user add` printed — `MASON_URL`, or `--url` — so it works beside `mason start` as against a `mason serve` elsewhere; TLS is as the URL's `sslmode`
|
|
186
|
+
says (a host off the machine: `require` when it names none). It never makes a user by itself: with no URL, it refuses
|
|
187
|
+
and says `mason user add claude`.
|
|
188
|
+
|
|
189
|
+
| Tool | Arguments | What it answers |
|
|
190
|
+
|---|---|---|
|
|
191
|
+
| `tables` | none | each table Mason serves, named as a statement writes it (`public.orders`, `public."Clients"`): `rows_about` (the copy's estimate), `kept` (how it is kept current, as `mason status` says it) and `kept_current_by` (`polling`, `logical replication`, `copying again`, or null when nothing keeps it current now) |
|
|
192
|
+
| `describe` | `table` (as `tables` names it, or a bare name in `public`) | its columns with their Postgres types and NOT NULL, its primary key, its foreign keys, comments, and the same rows and `kept` |
|
|
193
|
+
| `query` | `sql`, `max_rows` (200, at most 10,000), `timeout_ms` (30,000, at most 300,000) | `columns` (name, Postgres type), `rows` as Postgres's text, `truncated` when `max_rows` cut it; a refusal is a tool error with the SQLSTATE, message, DETAIL and HINT (the way out) |
|
|
194
|
+
| `explain` | `sql`, `analyze`, `timeout_ms` | Mason's EXPLAIN: each copy read with its rows and how current it is, the SQL it runs, the plan |
|
|
195
|
+
|
|
196
|
+
Its resources are `mason://help/agent` and `mason://help/mcp` (the two pages `mason help` prints). Every statement it
|
|
197
|
+
runs is in the request log under `application_name` `mason-mcp/<the client's name>` (`mason logs --requests`), a query
|
|
198
|
+
`max_rows` cut with the rows it sent; its catalog reads (the tables, a table's columns) are not, as no catalog read is.
|
|
199
|
+
A request the client cancels (`notifications/cancelled`) cancels its statement on the Postgres API.
|
|
200
|
+
|
|
201
|
+
Claude Code, in the project's folder with Mason running, the URL in the server's environment (the server's name
|
|
202
|
+
comes before `--env`, which takes every value after it):
|
|
203
|
+
|
|
204
|
+
```sh
|
|
205
|
+
mason user add claude # the URL, printed once
|
|
206
|
+
claude mcp add mason --env MASON_URL="postgres://claude:<password>@127.0.0.1:5433/shop" -- mason mcp
|
|
207
|
+
# or on its command line, where ps shows it to this machine's users and claude mcp add prints it
|
|
208
|
+
claude mcp add mason -- mason mcp --url "postgres://claude:<password>@127.0.0.1:5433/shop"
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Claude Desktop, in `claude_desktop_config.json`. The app often starts a server without your shell's `PATH`, where
|
|
212
|
+
neither `mason` nor the `node` its first line asks for is found: name both by their absolute paths (`which node`,
|
|
213
|
+
`which mason`):
|
|
214
|
+
|
|
215
|
+
```json
|
|
216
|
+
{
|
|
217
|
+
"mcpServers": {
|
|
218
|
+
"mason": {
|
|
219
|
+
"command": "/absolute/path/to/node",
|
|
220
|
+
"args": ["/absolute/path/to/mason", "mcp"],
|
|
221
|
+
"env": { "MASON_URL": "postgres://claude:<password>@127.0.0.1:5433/shop" }
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## Cleaning up
|
|
228
|
+
|
|
229
|
+
Three things exist once a project has run: your **files** (the sources file, in your app), the **copy of your data** (a
|
|
230
|
+
file in `.mason/`) and **Mason's state** (`.mason/`: its settings, logs and the request log). Each way of cleaning up
|
|
231
|
+
keeps a different set:
|
|
232
|
+
|
|
233
|
+
| You want | Run | Keeps |
|
|
234
|
+
|---|---|---|
|
|
235
|
+
| Pause | `mason stop` | the data, the state, your files: the next `mason start` resumes in seconds |
|
|
236
|
+
| Copy the data again, or free its disk | `mason destroy` | your files, `.mason/config.json` and the users: the next `mason start` copies everything again |
|
|
237
|
+
|
|
238
|
+
Restarting the machine is a `mason stop` that nobody ran: the next `mason start` resumes. In a demo project
|
|
239
|
+
(`mason init demo`) the demo database is state too: `mason destroy` removes it with the copy, and the next `mason start`
|
|
240
|
+
seeds it again.
|
|
241
|
+
|
|
242
|
+
## Requirements
|
|
243
|
+
|
|
244
|
+
`mason doctor` checks all of this and says what is missing.
|
|
245
|
+
|
|
246
|
+
- Node 22.13 or later. The SQL parser (`libpg-query`), the catalog (`PGlite`) and the engine (DuckDB) ship as
|
|
247
|
+
WebAssembly or prebuilt binaries: no native build, no Python or compiler needed.
|
|
248
|
+
- Memory: the copy may use half the container's memory limit by default, else half the machine's RAM
|
|
249
|
+
(`MASON_DUCKDB_MEMORY=4GB` sets it; `mason doctor` says it). Sizing a server:
|
|
250
|
+
[deploy/mason-data/README.md#memory](https://github.com/embeddable-hq/mason-data/blob/main/deploy/mason-data/README.md#memory).
|
|
251
|
+
- Postgres 13 or later. Each table needs a primary key, or a `key:` in the sources file (polling only).
|
|
252
|
+
|
|
253
|
+
## Security
|
|
254
|
+
|
|
255
|
+
`mason start`'s command server listens on 127.0.0.1 only; each request must carry the run's token, read from the
|
|
256
|
+
project's `.mason/config.json`, which a browser cannot send — or, for its web page (`mason ui`), the cookie the run's token is traded for on the first visit (HttpOnly, SameSite=Strict, its Host and every change's Origin checked). The Postgres API takes a user's password (`mason user`): a
|
|
257
|
+
MAC over its name keyed by the project's API key, so rotating the key removes every user at once. Off loopback the Postgres API is TLS or refused.
|
|
258
|
+
|
|
259
|
+
What a project keeps on this machine (the database URL, the API key, the run's token, the copy, its log and request
|
|
260
|
+
log) is in its `.mason/`, readable by your user only and gitignored by `mason init`.
|
|
261
|
+
|
|
262
|
+
## In production
|
|
263
|
+
|
|
264
|
+
Only the project folder travels — `mason/`, with `mason.yml` naming the Mason version it is written for. Production runs
|
|
265
|
+
it as one container, `ghcr.io/embeddable-hq/mason-data`, with `mason serve`: the same run, configured by the environment,
|
|
266
|
+
its copy on one volume, reading Postgres's change log (`MASON_CHANGES=replication`) or polling it. `mason help
|
|
267
|
+
production` is the road, offline; `deploy/mason-data/` in the repo is the Compose file and its walkthrough.
|
|
268
|
+
|
|
269
|
+
Apache-2.0.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A refusal the Postgres listener answers as an ErrorResponse: a login or a users file that would not do
|
|
3
|
+
* (src/duck/users.ts), with the HTTP-style status the listener maps to a SQLSTATE (src/api/postgres/server.ts
|
|
4
|
+
* `sqlstateOf`) and a stable machine-readable code.
|
|
5
|
+
*/
|
|
6
|
+
export class ApiError extends Error {
|
|
7
|
+
status;
|
|
8
|
+
/** The STABLE machine-readable name of the failure (`invalid_user`, `users_unreadable`, ...), carried in the
|
|
9
|
+
* ErrorResponse's detail so a reader branches on it instead of parsing `message`. */
|
|
10
|
+
code;
|
|
11
|
+
/** The user a refused `mason user` login asked for (src/duck/users.ts): its request-log row names it. */
|
|
12
|
+
userName;
|
|
13
|
+
constructor(status, message, code) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.status = status;
|
|
16
|
+
this.code = code;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE CATALOG'S WORKER: PGlite (Postgres as WebAssembly) runs HERE, on a worker thread, never on the listener's own
|
|
3
|
+
* (review of #480: a forwarded `SELECT pg_sleep(3600)` froze every other Postgres session, and PGlite ignores
|
|
4
|
+
* `statement_timeout`). The main thread (catalog.ts PgCatalog) posts one request at a
|
|
5
|
+
* time on the port and holds a deadline for it; past the deadline it TERMINATES this thread and starts another, so a
|
|
6
|
+
* statement that never ends costs one instance, not the process.
|
|
7
|
+
*
|
|
8
|
+
* Messages in: `{ id, op: "init", statements, optional? }` (start PGlite and run the DDL; the reply names the refused
|
|
9
|
+
* statement, or the optional ones refused),
|
|
10
|
+
* `{ id, op: "raw", bytes }` (frontend protocol bytes in, backend bytes out), `{ id, op: "query", sql }` (rows as
|
|
11
|
+
* objects, for the server's own lookups and the tests), `{ id, op: "close" }`. Every reply carries the id:
|
|
12
|
+
* `{ id, ok: true, bytes | rows }` or `{ id, ok: false, message }`.
|
|
13
|
+
*/
|
|
14
|
+
import { parentPort } from "node:worker_threads";
|
|
15
|
+
import { PGlite } from "@electric-sql/pglite";
|
|
16
|
+
import { citext } from "@electric-sql/pglite/contrib/citext";
|
|
17
|
+
const port = parentPort;
|
|
18
|
+
if (port === null)
|
|
19
|
+
throw new Error("catalog-worker.ts runs on a worker thread only");
|
|
20
|
+
const errorMessage = (e) => (e instanceof Error ? e.message : String(e));
|
|
21
|
+
let db = null;
|
|
22
|
+
port.on("message", (m) => {
|
|
23
|
+
void (async () => {
|
|
24
|
+
try {
|
|
25
|
+
switch (m.op) {
|
|
26
|
+
case "init": {
|
|
27
|
+
// citext bundled: a copied citext column is declared as itself (catalog.ts `catalogDdl`)
|
|
28
|
+
const instance = new PGlite({ extensions: { citext } });
|
|
29
|
+
await instance.waitReady;
|
|
30
|
+
for (const statement of m.statements) {
|
|
31
|
+
try {
|
|
32
|
+
await instance.exec(statement);
|
|
33
|
+
}
|
|
34
|
+
catch (e) {
|
|
35
|
+
// the half-made instance is discarded (the start failed, said below with the statement); a close that fails is said too
|
|
36
|
+
await instance.close().catch((ce) => console.error(`catalog worker: PGlite did not close cleanly after a refused DDL: ${errorMessage(ce)}`));
|
|
37
|
+
throw new Error(`the catalog DDL was refused (${errorMessage(e)}) at:\n${statement}`, { cause: e });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// a statement the catalog may do without (a source's column default or view, catalog.ts `catalogDefaults`,
|
|
41
|
+
// `catalogViews`): each on its own (a multi-statement one is its own implicit transaction: all or none), in
|
|
42
|
+
// passes while one more stands — a view over a view stands once the other does — and the refused ones named in
|
|
43
|
+
// the reply for the log
|
|
44
|
+
let left = [...(m.optional ?? [])];
|
|
45
|
+
let refused = [];
|
|
46
|
+
for (let progress = true; progress && left.length > 0;) {
|
|
47
|
+
refused = [];
|
|
48
|
+
for (const statement of left) {
|
|
49
|
+
try {
|
|
50
|
+
await instance.exec(statement);
|
|
51
|
+
}
|
|
52
|
+
catch (e) {
|
|
53
|
+
refused.push({ statement, message: errorMessage(e) });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
progress = refused.length < left.length;
|
|
57
|
+
left = refused.map((r) => r.statement);
|
|
58
|
+
}
|
|
59
|
+
db = instance;
|
|
60
|
+
port.postMessage({ id: m.id, ok: true, ...(refused.length > 0 ? { refused } : {}) });
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
case "raw": {
|
|
64
|
+
if (db === null)
|
|
65
|
+
throw new Error("the catalog is not started");
|
|
66
|
+
const bytes = await db.execProtocolRaw(m.bytes);
|
|
67
|
+
const copy = new Uint8Array(bytes); // PGlite may reuse its buffer: a copy of our own is transferred
|
|
68
|
+
port.postMessage({ id: m.id, ok: true, bytes: copy }, [copy.buffer]);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
case "query": {
|
|
72
|
+
if (db === null)
|
|
73
|
+
throw new Error("the catalog is not started");
|
|
74
|
+
const result = await db.query(m.sql);
|
|
75
|
+
port.postMessage({ id: m.id, ok: true, rows: result.rows });
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
case "close": {
|
|
79
|
+
const instance = db;
|
|
80
|
+
db = null;
|
|
81
|
+
if (instance !== null)
|
|
82
|
+
await instance.close();
|
|
83
|
+
port.postMessage({ id: m.id, ok: true });
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
catch (e) {
|
|
89
|
+
port.postMessage({ id: m.id, ok: false, message: errorMessage(e) });
|
|
90
|
+
}
|
|
91
|
+
})();
|
|
92
|
+
});
|