├── .gitignore
├── LICENSE
├── README.md
├── demo-src
└── demo.js
├── demo
├── demo.js
├── index.html
├── neon.svg
└── spinner.svg
├── dev
├── describe-c-to-js.mjs
└── describe-pre.js
├── index.d.ts
├── index.js
├── package-lock.json
├── package.json
├── src
└── describe.mjs
└── test
├── test-pagila-additions.sql
├── test.mjs
└── tests.txt
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Portions Copyright © 2023, Neon Inc.
2 | Portions Copyright © 1996-2023, The PostgreSQL Global Development Group
3 | Portions Copyright © 1994, The Regents of the University of California
4 |
5 | Permission to use, copy, modify, and distribute this software and its documentation for any purpose, without fee, and without a written agreement is hereby granted, provided that the above copyright notice and this paragraph and the following two paragraphs appear in all copies.
6 |
7 | IN NO EVENT SHALL ANY OF THE COPYRIGHT HOLDERS NAMED ABOVE BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF THE COPYRIGHT HOLDERS HAVE BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
8 |
9 | THE COPYRIGHT HOLDERS SPECIFICALLY DISCLAIM ANY WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON AN “AS IS” BASIS, AND THE COPYRIGHT HOLDERS HAVE NO OBLIGATIONS TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
10 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # psql-describe
2 |
3 | psql's `\d` (describe) family of commands ported to JavaScript.
4 |
5 | * From Postgres `REL_17_0`, we take `exec_command_d`, `exec_command_list` and `exec_command_sf_sv` from `command.c`, and all of `describe.c` and `sql_help.c`, from `src/bin/psql` (note that `sql_help.c` is generated during compilation of psql: you need to run `./configure` in the Postgres repo root followed by `make` in `src/bin/psql`).
6 | * We use plenty of RegExp search-and-replace to turn this C code into valid JS syntax.
7 | * We implement some C library functions, such as `strlen` and `strchr`, and some Postgres support functions, such as `printTable` and `printQuery`, in JavaScript.
8 | * We write tests to catch problems, mostly related to pointer arithmetic, pointer dereferencing, and pointer output parameters. Then we fix them.
9 |
10 | This approach means that some of the 13,000+ lines of code in `describe.mjs` may never actually have been looked at. If you find a bug, please file an issue.
11 |
12 | This library is [on npm](https://www.npmjs.com/package/psql-describe) and [powers backslash commands in the Neon SQL Editor](https://neon.tech/blog/bringing-psqls-d-to-your-web-browser).
13 |
14 | ## Usage
15 |
16 | The key export is the `describe()` function:
17 |
18 | ```typescript
19 | describe(
20 | cmd,
21 | dbName,
22 | runQuery,
23 | outputFn,
24 | echoHidden = false,
25 | sversion = null,
26 | std_strings = true,
27 | docsURLTemplate = (id) => `https://www.postgresql.org/docs/current/${id}.html`,
28 | ): { promise, cancel };
29 | ```
30 |
31 | * `cmd` (string) is the desired describe command, including the leading backslash, such as `\d` (don't forget you may need to escape the backslash in a literal string).
32 | * `dbName` (string) is the name of the connected database.
33 | * `runQuery` is an async function that takes a SQL query (string) and must return *unparsed* query results in the same format used by node-postgres when specifying `rowMode: 'array'`.
34 | * `outputFn` is a function that receives output for display: this output will be either a string or a table object (see below).
35 | * `echoHidden` (boolean) has the same effect as the `-E` argument to psql: if `true`, all SQL queries are output to `outputFn`, in addition to the final results.
36 | * `sversion` (number) should be the same value as `SHOW server_version_num` executed on the server. It is used to determine what features the database supports. If it is not provided, the server is queried for it.
37 | * `std_strings` (boolean) indicates the value of `standard_conforming_strings` in the database.
38 | * `docsURLTemplate` (function) specifies how a docs page ID is transformed into a URL, for use with `\h`.
39 |
40 | The function returns an object with two keys: `{ promise, cancel }`:
41 |
42 | * `promise` is a `Promise` that resolves when the command completes.
43 | * `cancel()` is a function you can call to abort the command.
44 |
45 | The outputs of `describe()`, as passed to the `outputFn` argument, are a mix of plain strings and JS objects representing tables.
46 |
47 | To format these outputs for display, two additional functions are exported:
48 |
49 | * ```describeDataToString(item)```
50 |
51 | This function passes though string items unchanged. When an object item is passed in, a formatted plain-text table is returned, identical to those produced by the psql CLI.
52 |
53 | * ```describeDataToHtml(item)```
54 |
55 | This function HTML-escapes string items, and formats object items as HTML tables (whose contents are HTML-escaped).
56 |
57 |
58 | ## Tests
59 |
60 | The tests compare this software's output against `psql` (release 17.0) for the commands in `test/tests.txt`. Output should be character-for-character identical, except for differences in trailing whitespace.
61 |
62 | In case of failure, the tests halt and a `psql.txt` and `local.txt` are written, which you can then `diff`.
63 |
64 | To make the tests work on your machine, you'll need to create a test database (see below), and probably update the DB connection strings in the `test` command in `package.json`.
65 |
66 |
67 | ### Database
68 |
69 | Tests should be run against a database named `psqldescribe` containing the [Pagila](https://github.com/devrimgunduz/pagila) data set, with a few additions:
70 |
71 | ```bash
72 | curl https://raw.githubusercontent.com/devrimgunduz/pagila/master/pagila-schema.sql | psql psqldescribe
73 | curl https://raw.githubusercontent.com/devrimgunduz/pagila/master/pagila-data.sql | psql psqldescribe
74 | psql psqldescribe < test/test-pagila-additions.sql
75 | ```
76 |
77 | ## License
78 |
79 | This package is released under the [Postgres license](LICENSE).
80 |
--------------------------------------------------------------------------------
/demo-src/demo.js:
--------------------------------------------------------------------------------
1 | import { describe, describeDataToString, describeDataToHtml } from '../src/describe.mjs';
2 |
3 | function parse(url, parseQueryString) {
4 | const { protocol } = new URL(url);
5 | // we now swap the protocol to http: so that `new URL()` will parse it fully
6 | const httpUrl = 'http:' + url.substring(protocol.length);
7 | let { username, password, host, hostname, port, pathname, search, searchParams, hash } = new URL(httpUrl);
8 | password = decodeURIComponent(password);
9 | const auth = username + ':' + password;
10 | const query = parseQueryString ? Object.fromEntries(searchParams.entries()) : search;
11 | return { href: url, protocol, auth, username, password, host, hostname, port, pathname, search, query, hash };
12 | }
13 |
14 | let cancelFn;
15 |
16 | function end() {
17 | goBtn.value = goBtnUsualTitle;
18 | spinner.style.display = 'none';
19 | cancelFn = undefined;
20 | }
21 |
22 | async function go() {
23 | if (cancelFn === undefined) {
24 | goBtn.value = "Cancel";
25 | spinner.style.display = 'block';
26 |
27 | const connectionString = document.querySelector('#dburl').value;
28 | let dbName, dbHost;
29 | try {
30 | const parsedConnnectionString = parse(connectionString);
31 | dbName = parsedConnnectionString.pathname.slice(1);
32 | dbHost = parsedConnnectionString.hostname;
33 |
34 | } catch (err) {
35 | alert('Invalid database URL');
36 | end();
37 | return;
38 | }
39 |
40 | const
41 | cmd = document.querySelector('#sql').value,
42 | echoHidden = document.querySelector('#echohidden').checked,
43 | htmlOutput = document.querySelector('#html').checked;
44 |
45 | sessionStorage.setItem('form', JSON.stringify({ connectionString, cmd, echoHidden, htmlOutput }));
46 |
47 | const headers = {
48 | 'Neon-Connection-String': connectionString,
49 | 'Neon-Raw-Text-Output': 'true', // because we want raw Postgres text format
50 | 'Neon-Array-Mode': 'true', // this saves data and post-processing even if we return objects, not arrays
51 | 'Neon-Pool-Opt-In': 'true',
52 | };
53 |
54 | queryFn = async (sql) => {
55 | const response = await fetch(`https://${dbHost}/sql`, {
56 | method: 'POST',
57 | body: JSON.stringify({ query: sql, params: [] }),
58 | headers,
59 | });
60 |
61 | const json = await response.json();
62 | if (response.status === 200) return json;
63 |
64 | const
65 | jsonMsg = json.message,
66 | msgMatch = jsonMsg.match(/ERROR: (.*?)\n/),
67 | errMsg = msgMatch ? msgMatch[1] : jsonMsg;
68 |
69 | throw new Error(errMsg);
70 | }
71 |
72 | let outputEl = document.querySelector('#output');
73 | outputEl.innerHTML = '';
74 | if (!htmlOutput) outputEl = outputEl.appendChild(document.createElement('pre'));
75 |
76 | let firstOutput = true;
77 | const outputFn = htmlOutput ?
78 | x => outputEl.innerHTML += describeDataToHtml(x) :
79 | x => {
80 | outputEl.innerHTML += (firstOutput ? '' : '\n\n') + describeDataToString(x, true);
81 | firstOutput = false;
82 | };
83 |
84 | const { promise, cancel } = describe(cmd, dbName, queryFn, outputFn, echoHidden);
85 | cancelFn = cancel;
86 | const status = await promise;
87 |
88 | if (status !== null) end(); // if this query was cancelled, ignore that it returned; otherwise:
89 |
90 | } else {
91 | cancelFn();
92 | end();
93 | }
94 | }
95 |
96 | window.addEventListener('load', () => {
97 | const saveData = sessionStorage.getItem('form');
98 | if (!saveData) return;
99 | const { connectionString, cmd, echoHidden, htmlOutput } = JSON.parse(saveData);
100 | document.querySelector('#dburl').value = connectionString;
101 | document.querySelector('#sql').value = cmd;
102 | document.querySelector('#echohidden').checked = echoHidden;
103 | document.querySelector('#html').checked = htmlOutput;
104 | });
105 |
106 | const goBtn = document.querySelector('#gobtn');
107 | const goBtnUsualTitle = goBtn.value;
108 | goBtn.addEventListener('click', go);
109 |
110 | document.querySelector('#sql').addEventListener('keyup', (e) => {
111 | if (e.key === "Enter") go();
112 | e.preventDefault();
113 | })
114 |
115 | document.querySelector('#examples').addEventListener('click', (e) => {
116 | if (e.target.nodeName === 'A') document.querySelector('#sql').value = e.target.textContent;
117 | e.preventDefault();
118 | });
119 |
120 | const spinner = document.querySelector('#spinner');
121 |
122 |
123 |
--------------------------------------------------------------------------------
/demo/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
74 |

75 |
76 | Database URL
77 |
78 |
79 |
80 | Describe command
81 |
82 |
83 | Examples: \? \l \d+ \d pg_class* \du+ \dconfig \dS+ \dfS \sf date_trunc(text, interval) \sv+ pg_stats
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |

94 |
95 | Source on GitHub
96 |
97 |
--------------------------------------------------------------------------------
/demo/neon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/demo/spinner.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/dev/describe-c-to-js.mjs:
--------------------------------------------------------------------------------
1 | import fs from 'fs';
2 |
3 | const command_c = fs.readFileSync('command.c', { encoding: 'utf8'});
4 | const describe_c = fs.readFileSync('describe.c', { encoding: 'utf8'});
5 | const pre_js = fs.readFileSync('describe-pre.js', { encoding: 'utf8'});
6 |
7 | const exec_command_d = command_c.match(/static\s+backslashResult\s+exec_command_d\(.*\s+\{[\s\S]+?^\}[\s\S]+?^\}/m)[0];
8 | const describe_commands = describe_c.match(/\/\*\s+\* \\da[\s\S]+/)[0];
9 |
10 | let js = `
11 | /* from command.c */
12 |
13 | ${exec_command_d}
14 |
15 | /* from describe.c */
16 |
17 | ${describe_commands}
18 | `;
19 |
20 | // remove validateSQLNamePattern in favour of tweaked implementation below
21 | js = js.replace(/static bool\s*validateSQLNamePattern[\s\S]*?\n\}/, '// re-implemented manually elsewhere');
22 |
23 | // replace function return types with '[async] function', and remove argument types
24 | const asyncFunctions = ['PSQLexec'];
25 | js = js.replace(/(^\w+.*\n)+(^\w+)\(([^)]*)\)/mg, (m, g1, g2, g3) => {
26 | const isAsync = g2 !== 'map_typename_pattern' && g2 !== 'add_role_attribute';
27 | if (isAsync) asyncFunctions.push(g2);
28 | return `${isAsync ? 'async ' : ''}function ${g2}(${g3.split(',').map(arg => arg.match(/\w+\s*$/)[0]).join(', ')})`
29 | });
30 |
31 | // await async function calls
32 | js = js.replace(new RegExp('(? {
36 | const parts = m.split(/[\s,;]+/).filter(s => s !== '');
37 | let result = parts.shift();
38 | while (parts.length) result += ` ${parts.shift()} = { /* struct */ }${parts.length ? ',' : ';\n'}`
39 | return result;
40 | });
41 |
42 | // replace variable types with 'let'
43 | js = js.replace(/(^|for\s*\()(\s*)(static\s+)?(const\s+)?(bool|char|int|int16|Oid|backslashResult|PGresult|PQExpBufferData|printQueryOpt|printTableOpt|printTableContent)(\s*\*)?(\s*const)?/mg, '$1$2let '),
44 |
45 | // remove '&' reference operator
46 | js = js.replace(/&(\w)/g, '$1');
47 |
48 | // replace constants inside strings to be concatenated
49 | const defineStrs = {
50 | RELKIND_RELATION: 'r',
51 | RELKIND_INDEX: 'i',
52 | RELKIND_SEQUENCE: 'S',
53 | RELKIND_TOASTVALUE: 't',
54 | RELKIND_VIEW: 'v',
55 | RELKIND_MATVIEW: 'm',
56 | RELKIND_COMPOSITE_TYPE: 'c',
57 | RELKIND_FOREIGN_TABLE: 'f',
58 | RELKIND_PARTITIONED_TABLE: 'p',
59 | RELKIND_PARTITIONED_INDEX: 'I',
60 | RELPERSISTENCE_PERMANENT: 'p',
61 | RELPERSISTENCE_UNLOGGED: 'u',
62 | RELPERSISTENCE_TEMP: 't',
63 | REPLICA_IDENTITY_DEFAULT: 'd',
64 | REPLICA_IDENTITY_NOTHING: 'n',
65 | REPLICA_IDENTITY_FULL: 'f',
66 | REPLICA_IDENTITY_INDEX: 'i',
67 | }
68 | js = js.replace(new RegExp(`CppAsString2\\((${Object.keys(defineStrs).join('|')})\\)`, 'g'), (m, g1) => `\n"'${defineStrs[g1]}'"\n`);
69 |
70 | // concatenate consecutive strings
71 | js = js.replace(/"\s*\n(\s*)"/g, '" +\n$1"');
72 |
73 | // array declarations: remove subscripts
74 | js = js.replace(/^(\s*let\s+\w+)\[([0-9]*|[A-Z_]*)\]/gm, '$1');
75 |
76 | // array literals: {} -> []
77 | js = js.replace(/([^'])\{(\S[^}]*)\}/g, '$1[$2]');
78 |
79 | // short-circuit error returns
80 | js = js.replace(/goto error_return;/g, 'return false;');
81 |
82 | // special case for typename_map: replace {...} with [...]
83 | js = js.replace(/(let\s*typename_map\s*=\s*)\{([^}]+)\}/, '$1[$2]');
84 |
85 | // struct defs -> empty {}
86 | js = js.replace(/struct\s*\{[^}]*\}\s*(\w+)/, 'let $1 = {}');
87 |
88 | // struct pointer access operator (->) to .
89 | js = js.replace(/->/g, '.');
90 |
91 | // remove bracketed dereferences
92 | js = js.replace(/\*\(/g, '(');
93 |
94 | // remove char casts
95 | js = js.replace(/\(char \*\*?\)/g, '');
96 |
97 | // remove additional dereferences
98 | js = js.replace(/([^%])\*(\w)/g, '$1$2');
99 |
100 | // special cases
101 | js = js.replace(/let headers;/, 'let headers = [];');
102 | js = js.replace('indexdef = usingpos + 7;', 'indexdef = indexdef.slice(usingpos + 7);');
103 | js = js.replace('tgdef = usingpos + 9;', 'tgdef = tgdef.slice(usingpos + 9);');
104 | js = js.replace(/(\w+)\[0\] == '\\0'/g, "($1[0] == '\\0' || $1[0] === undefined)");
105 |
106 | js = `
107 | ${pre_js}
108 | ${js}
109 | `;
110 |
111 | console.log(js);
112 |
--------------------------------------------------------------------------------
/dev/describe-pre.js:
--------------------------------------------------------------------------------
1 | const NULL = null;
2 | const FUNC_MAX_ARGS = 100;
3 | const ESCAPE_STRING_SYNTAX = 'E';
4 | const cancel_pressed = false;
5 |
6 | const
7 | EXIT_FAILURE = 1,
8 | EXIT_SUCCESS = 0;
9 |
10 | const
11 | PSQL_CMD_UNKNOWN = 0, /* not done parsing yet (internal only) */
12 | PSQL_CMD_SEND = 1, /* query complete; send off */
13 | PSQL_CMD_SKIP_LINE = 2, /* keep building query */
14 | PSQL_CMD_TERMINATE = 3, /* quit program */
15 | PSQL_CMD_NEWEDIT = 4, /* query buffer was changed (e.g., via \e) */
16 | PSQL_CMD_ERROR = 5; /* the execution of the backslash command */
17 |
18 | const
19 | OT_NORMAL = 0, /* normal case */
20 | OT_SQLID = 1, /* treat as SQL identifier */
21 | OT_SQLIDHACK = 2, /* SQL identifier, but don't downcase */
22 | OT_FILEPIPE = 3, /* it's a filename or pipe */
23 | OT_WHOLE_LINE = 4; /* just snarf the rest of the line */
24 |
25 | const PG_UTF8 = 6;
26 |
27 | const
28 | CONNECTION_OK = 0,
29 | CONNECTION_BAD = 1,
30 | CONNECTION_STARTED = 2, /* Waiting for connection to be made. */
31 | CONNECTION_MADE = 3, /* Connection OK; waiting to send. */
32 | CONNECTION_AWAITING_RESPONSE = 4, /* Waiting for a response from the postmaster. */
33 | CONNECTION_AUTH_OK = 5, /* Received authentication; waiting for backend startup. */
34 | CONNECTION_SETENV = 6, /* This state is no longer used. */
35 | CONNECTION_SSL_STARTUP = 7, /* Negotiating SSL. */
36 | CONNECTION_NEEDED = 8, /* Internal state: connect() needed */
37 | CONNECTION_CHECK_WRITABLE = 9, /* Checking if session is read-write. */
38 | CONNECTION_CONSUME = 10, /* Consuming any extra messages. */
39 | CONNECTION_GSS_STARTUP = 11, /* Negotiating GSSAPI. */
40 | CONNECTION_CHECK_TARGET = 12, /* Checking target server properties. */
41 | CONNECTION_CHECK_STANDBY = 13; /* Checking if server is in standby mode. */
42 |
43 | const
44 | COERCION_METHOD_FUNCTION = 'f', /* use a function */
45 | COERCION_METHOD_BINARY = 'b', /* types are binary-compatible */
46 | COERCION_METHOD_INOUT = 'i'; /* use input/output functions */
47 |
48 | const
49 | COERCION_CODE_IMPLICIT = 'i', /* coercion in context of expression */
50 | COERCION_CODE_ASSIGNMENT = 'a', /* coercion in context of assignment */
51 | COERCION_CODE_EXPLICIT = 'e'; /* explicit cast operation */
52 |
53 | const
54 | DEFACLOBJ_RELATION = 'r', /* table, view */
55 | DEFACLOBJ_SEQUENCE = 'S', /* sequence */
56 | DEFACLOBJ_FUNCTION = 'f', /* function */
57 | DEFACLOBJ_TYPE = 'T', /* type */
58 | DEFACLOBJ_NAMESPACE = 'n'; /* namespace */
59 |
60 | const
61 | ATTRIBUTE_IDENTITY_ALWAYS = 'a',
62 | ATTRIBUTE_IDENTITY_BY_DEFAULT = 'd',
63 | ATTRIBUTE_GENERATED_STORED = 's';
64 |
65 | const
66 | RELKIND_RELATION = 'r',
67 | RELKIND_INDEX = 'i',
68 | RELKIND_SEQUENCE = 'S',
69 | RELKIND_TOASTVALUE = 't',
70 | RELKIND_VIEW = 'v',
71 | RELKIND_MATVIEW = 'm',
72 | RELKIND_COMPOSITE_TYPE = 'c',
73 | RELKIND_FOREIGN_TABLE = 'f',
74 | RELKIND_PARTITIONED_TABLE = 'p',
75 | RELKIND_PARTITIONED_INDEX = 'I',
76 | RELPERSISTENCE_PERMANENT = 'p',
77 | RELPERSISTENCE_UNLOGGED = 'u',
78 | RELPERSISTENCE_TEMP = 't',
79 | REPLICA_IDENTITY_DEFAULT = 'd',
80 | REPLICA_IDENTITY_NOTHING = 'n',
81 | REPLICA_IDENTITY_FULL = 'f',
82 | REPLICA_IDENTITY_INDEX = 'i';
83 |
84 | const
85 | BOOLOID = 16,
86 | BYTEAOID = 17,
87 | CHAROID = 18,
88 | NAMEOID = 19,
89 | INT8OID = 20,
90 | INT2OID = 21,
91 | INT2VECTOROID = 22,
92 | INT4OID = 23,
93 | REGPROCOID = 24,
94 | TEXTOID = 25,
95 | OIDOID = 26,
96 | TIDOID = 27,
97 | XIDOID = 28,
98 | CIDOID = 29,
99 | OIDVECTOROID = 30,
100 | JSONOID = 114,
101 | XMLOID = 142,
102 | PG_NODE_TREEOID = 194,
103 | PG_NDISTINCTOID = 3361,
104 | PG_DEPENDENCIESOID = 3402,
105 | PG_MCV_LISTOID = 5017,
106 | PG_DDL_COMMANDOID = 32,
107 | XID8OID = 5069,
108 | POINTOID = 600,
109 | LSEGOID = 601,
110 | PATHOID = 602,
111 | BOXOID = 603,
112 | POLYGONOID = 604,
113 | LINEOID = 628,
114 | FLOAT4OID = 700,
115 | FLOAT8OID = 701,
116 | UNKNOWNOID = 705,
117 | CIRCLEOID = 718,
118 | MONEYOID = 790,
119 | MACADDROID = 829,
120 | INETOID = 869,
121 | CIDROID = 650,
122 | MACADDR8OID = 774,
123 | ACLITEMOID = 1033,
124 | BPCHAROID = 1042,
125 | VARCHAROID = 1043,
126 | DATEOID = 1082,
127 | TIMEOID = 1083,
128 | TIMESTAMPOID = 1114,
129 | TIMESTAMPTZOID = 1184,
130 | INTERVALOID = 1186,
131 | TIMETZOID = 1266,
132 | BITOID = 1560,
133 | VARBITOID = 1562,
134 | NUMERICOID = 1700,
135 | REFCURSOROID = 1790,
136 | REGPROCEDUREOID = 2202,
137 | REGOPEROID = 2203,
138 | REGOPERATOROID = 2204,
139 | REGCLASSOID = 2205,
140 | REGCOLLATIONOID = 4191,
141 | REGTYPEOID = 2206,
142 | REGROLEOID = 4096,
143 | REGNAMESPACEOID = 4089,
144 | UUIDOID = 2950,
145 | PG_LSNOID = 3220,
146 | TSVECTOROID = 3614,
147 | GTSVECTOROID = 3642,
148 | TSQUERYOID = 3615,
149 | REGCONFIGOID = 3734,
150 | REGDICTIONARYOID = 3769,
151 | JSONBOID = 3802,
152 | JSONPATHOID = 4072,
153 | TXID_SNAPSHOTOID = 2970,
154 | PG_SNAPSHOTOID = 5038,
155 | INT4RANGEOID = 3904,
156 | NUMRANGEOID = 3906,
157 | TSRANGEOID = 3908,
158 | TSTZRANGEOID = 3910,
159 | DATERANGEOID = 3912,
160 | INT8RANGEOID = 3926,
161 | INT4MULTIRANGEOID = 4451,
162 | NUMMULTIRANGEOID = 4532,
163 | TSMULTIRANGEOID = 4533,
164 | TSTZMULTIRANGEOID = 4534,
165 | DATEMULTIRANGEOID = 4535,
166 | INT8MULTIRANGEOID = 4536,
167 | RECORDOID = 2249,
168 | RECORDARRAYOID = 2287,
169 | CSTRINGOID = 2275,
170 | ANYOID = 2276,
171 | ANYARRAYOID = 2277,
172 | VOIDOID = 2278,
173 | TRIGGEROID = 2279,
174 | EVENT_TRIGGEROID = 3838,
175 | LANGUAGE_HANDLEROID = 2280,
176 | INTERNALOID = 2281,
177 | ANYELEMENTOID = 2283,
178 | ANYNONARRAYOID = 2776,
179 | ANYENUMOID = 3500,
180 | FDW_HANDLEROID = 3115,
181 | INDEX_AM_HANDLEROID = 325,
182 | TSM_HANDLEROID = 3310,
183 | TABLE_AM_HANDLEROID = 269,
184 | ANYRANGEOID = 3831,
185 | ANYCOMPATIBLEOID = 5077,
186 | ANYCOMPATIBLEARRAYOID = 5078,
187 | ANYCOMPATIBLENONARRAYOID = 5079,
188 | ANYCOMPATIBLERANGEOID = 5080,
189 | ANYMULTIRANGEOID = 4537,
190 | ANYCOMPATIBLEMULTIRANGEOID = 4538,
191 | PG_BRIN_BLOOM_SUMMARYOID = 4600,
192 | PG_BRIN_MINMAX_MULTI_SUMMARYOID = 4601,
193 | BOOLARRAYOID = 1000,
194 | BYTEAARRAYOID = 1001,
195 | CHARARRAYOID = 1002,
196 | NAMEARRAYOID = 1003,
197 | INT8ARRAYOID = 1016,
198 | INT2ARRAYOID = 1005,
199 | INT2VECTORARRAYOID = 1006,
200 | INT4ARRAYOID = 1007,
201 | REGPROCARRAYOID = 1008,
202 | TEXTARRAYOID = 1009,
203 | OIDARRAYOID = 1028,
204 | TIDARRAYOID = 1010,
205 | XIDARRAYOID = 1011,
206 | CIDARRAYOID = 1012,
207 | OIDVECTORARRAYOID = 1013,
208 | PG_TYPEARRAYOID = 210,
209 | PG_ATTRIBUTEARRAYOID = 270,
210 | PG_PROCARRAYOID = 272,
211 | PG_CLASSARRAYOID = 273,
212 | JSONARRAYOID = 199,
213 | XMLARRAYOID = 143,
214 | XID8ARRAYOID = 271,
215 | POINTARRAYOID = 1017,
216 | LSEGARRAYOID = 1018,
217 | PATHARRAYOID = 1019,
218 | BOXARRAYOID = 1020,
219 | POLYGONARRAYOID = 1027,
220 | LINEARRAYOID = 629,
221 | FLOAT4ARRAYOID = 1021,
222 | FLOAT8ARRAYOID = 1022,
223 | CIRCLEARRAYOID = 719,
224 | MONEYARRAYOID = 791,
225 | MACADDRARRAYOID = 1040,
226 | INETARRAYOID = 1041,
227 | CIDRARRAYOID = 651,
228 | MACADDR8ARRAYOID = 775,
229 | ACLITEMARRAYOID = 1034,
230 | BPCHARARRAYOID = 1014,
231 | VARCHARARRAYOID = 1015,
232 | DATEARRAYOID = 1182,
233 | TIMEARRAYOID = 1183,
234 | TIMESTAMPARRAYOID = 1115,
235 | TIMESTAMPTZARRAYOID = 1185,
236 | INTERVALARRAYOID = 1187,
237 | TIMETZARRAYOID = 1270,
238 | BITARRAYOID = 1561,
239 | VARBITARRAYOID = 1563,
240 | NUMERICARRAYOID = 1231,
241 | REFCURSORARRAYOID = 2201,
242 | REGPROCEDUREARRAYOID = 2207,
243 | REGOPERARRAYOID = 2208,
244 | REGOPERATORARRAYOID = 2209,
245 | REGCLASSARRAYOID = 2210,
246 | REGCOLLATIONARRAYOID = 4192,
247 | REGTYPEARRAYOID = 2211,
248 | REGROLEARRAYOID = 4097,
249 | REGNAMESPACEARRAYOID = 4090,
250 | UUIDARRAYOID = 2951,
251 | PG_LSNARRAYOID = 3221,
252 | TSVECTORARRAYOID = 3643,
253 | GTSVECTORARRAYOID = 3644,
254 | TSQUERYARRAYOID = 3645,
255 | REGCONFIGARRAYOID = 3735,
256 | REGDICTIONARYARRAYOID = 3770,
257 | JSONBARRAYOID = 3807,
258 | JSONPATHARRAYOID = 4073,
259 | TXID_SNAPSHOTARRAYOID = 2949,
260 | PG_SNAPSHOTARRAYOID = 5039,
261 | INT4RANGEARRAYOID = 3905,
262 | NUMRANGEARRAYOID = 3907,
263 | TSRANGEARRAYOID = 3909,
264 | TSTZRANGEARRAYOID = 3911,
265 | DATERANGEARRAYOID = 3913,
266 | INT8RANGEARRAYOID = 3927,
267 | INT4MULTIRANGEARRAYOID = 6150,
268 | NUMMULTIRANGEARRAYOID = 6151,
269 | TSMULTIRANGEARRAYOID = 6152,
270 | TSTZMULTIRANGEARRAYOID = 6153,
271 | DATEMULTIRANGEARRAYOID = 6155,
272 | INT8MULTIRANGEARRAYOID = 6157,
273 | CSTRINGARRAYOID = 1263;
274 |
275 | const pset = {};
276 | let PSQLexec;
277 |
278 | export async function describe(cmd, dbName, runQuery, sversion = 140000, std_strings = 1) { // cmd should be: `\dxxx xxxxx xxxxx`
279 | PSQLexec = sql => runQuery(trimTrailingNull(sql));
280 | pset.sversion = sversion;
281 | pset.db = {
282 | dbName,
283 | sversion,
284 | std_strings,
285 | status: CONNECTION_OK,
286 | encoding: PG_UTF8
287 | }; // PGconn struct
288 | pset.popt = { // print options
289 | topt: {},
290 | nullPrint: '',
291 | };
292 | const match = cmd.match(/^\\(d\S*)(.*)/);
293 | if (!match) {
294 | console.log(`unsupported describe command: ${cmd}`);
295 | return null;
296 | }
297 | let [, dCmd, remaining] = match;
298 | dCmd += '\0';
299 | remaining += '\0';
300 |
301 | const scan_state = [remaining, 0];
302 | const result = await exec_command_d(scan_state, true, dCmd);
303 | // TODO: implement \?, \h, etc.
304 | if (result === PSQL_CMD_UNKNOWN) console.log(`invalid command \\${dCmd}\ntry \\? for help.`);
305 | }
306 |
307 | function gettext_noop(x) { return x; }
308 |
309 | function strchr(str, chr) {
310 | return strstr(str, chr);
311 | }
312 |
313 | function strstr(str1, str2) { // this is not a fully general implementation, but it usually works
314 | const index = str1.indexOf(trimTrailingNull(str2));
315 | return index === -1 ? NULL : index;
316 | }
317 |
318 | function strlen(str) {
319 | const nullIndex = str.indexOf('\0');
320 | return nullIndex === -1 ? str.length : nullIndex;
321 | }
322 |
323 | function strcmp(s1, s2) {
324 | return strncmp(s1, s2, Infinity);
325 | }
326 |
327 | function strncmp(s1, s2, n) {
328 | if (typeof s1 !== 'string' || typeof s2 !== 'string') throw new Error('Not a string');
329 | s1 = trimTrailingNull(s1);
330 | if (s1.length > n) s1 = s1.slice(0, n);
331 | s2 = trimTrailingNull(s2);
332 | if (s2.length > n) s2 = s2.slice(0, n);
333 | return s1 < s2 ? -1 : s1 > s2 ? 1 : 0;
334 | }
335 |
336 | function strspn(str, chrs) {
337 | const len = strlen(str);
338 | for (let i = 0; i < len; i++) {
339 | if (chrs.indexOf(str[i]) === -1) return i;
340 | }
341 | return len;
342 | }
343 |
344 | function atoi(str) {
345 | return parseInt(str, 10);
346 | }
347 |
348 | function atooid(str) {
349 | return parseInt(str, 10);
350 | }
351 |
352 | function pg_strdup(str) {
353 | return str;
354 | }
355 |
356 | function isWhitespace(chr) {
357 | return chr === ' ' || chr === '\t' || chr === '\n' || chr === '\r';
358 | }
359 |
360 | function isQuote(chr) {
361 | return chr === '"' || chr === "'";
362 | }
363 |
364 | function PQdb(conn) {
365 | if (!conn) return NULL;
366 | return conn.dbName;
367 | }
368 |
369 | function PQserverVersion(conn) {
370 | if (!conn) return 0;
371 | if (conn.status === CONNECTION_BAD) return 0;
372 | return conn.sversion;
373 | }
374 |
375 | function PQclientEncoding(conn) {
376 | if (!conn || conn.status != CONNECTION_OK) return -1;
377 | return conn.client_encoding;
378 | }
379 |
380 | function PQntuples(res) {
381 | return res.rowCount;
382 | }
383 |
384 | function PQnfields(res) {
385 | return res.fields.length;
386 | }
387 |
388 | function PQfname(res, field_num) {
389 | return res.fields[field_num].name;
390 | }
391 |
392 | function PQftype(res, field_num) {
393 | return res.fields[field_num].dataTypeID;
394 | }
395 |
396 | function PQgetisnull(res, tup_num, field_num) {
397 | return res.rows[tup_num][field_num] === null ? 1 : 0;
398 | }
399 |
400 | function PQgetvalue(res, tup_num, field_num) {
401 | const val = res.rows[tup_num][field_num];
402 | return String(val === null ? '' : val);
403 | }
404 |
405 | /*
406 | * PQfnumber: find column number given column name
407 | *
408 | * The column name is parsed as if it were in a SQL statement, including
409 | * case-folding and double-quote processing. But note a possible gotcha:
410 | * downcasing in the frontend might follow different locale rules than
411 | * downcasing in the backend...
412 | *
413 | * Returns -1 if no match. In the present backend it is also possible
414 | * to have multiple matches, in which case the first one is found.
415 | */
416 | function PQfnumber(res, field_name) {
417 | let in_quotes;
418 | let iptr;
419 | let optr;
420 | let i;
421 | let len;
422 |
423 | if (!res) return -1;
424 |
425 | /*
426 | * Note: it is correct to reject a zero-length input string; the proper
427 | * input to match a zero-length field name would be "".
428 | */
429 | if (field_name == NULL || field_name[0] == '\0') return -1;
430 |
431 | /*
432 | * Note: this code will not reject partially quoted strings, eg
433 | * foo"BAR"foo will become fooBARfoo when it probably ought to be an error
434 | * condition.
435 | */
436 |
437 | in_quotes = false;
438 | optr = '';
439 | for (iptr = 0, len = strlen(field_name); iptr < len; iptr++) {
440 | let c = field_name[iptr];
441 |
442 | if (in_quotes) {
443 | if (c == '"') {
444 | if (field_name[iptr + 1] == '"') {
445 | /* doubled quotes become a single quote */
446 | optr += '"';
447 | iptr++;
448 | }
449 | else
450 | in_quotes = false;
451 | }
452 | else
453 | optr += c;
454 | }
455 | else if (c == '"')
456 | in_quotes = true;
457 | else {
458 | c = pg_tolower(c);
459 | optr += c;
460 | }
461 | }
462 | optr += '\0';
463 |
464 | for (i = 0, len = PQnfields(res); i < len; i++) {
465 | if (strcmp(optr, PQfname(res, i)) == 0) {
466 | return i;
467 | }
468 | }
469 |
470 | return -1;
471 | }
472 |
473 | function pg_wcswidth(pwcs, len, encoding) {
474 | return len;
475 | }
476 |
477 | function sizeof(x) {
478 | return 0;
479 | }
480 |
481 | function _(str) {
482 | return str;
483 | }
484 |
485 | function formatPGVersionNumber(version_number, include_minor, buf, buflen) {
486 | if (version_number >= 100000) {
487 | /* New two-part style */
488 | if (include_minor)
489 | buf = sprintf("%d.%d", Math.floor(version_number / 10000),
490 | version_number % 10000);
491 | else
492 | buf = sprintf("%d", version_number / 10000);
493 | }
494 | else {
495 | /* Old three-part style */
496 | if (include_minor)
497 | buf = sprintf("%d.%d.%d", Math.floor(version_number / 10000),
498 | Math.floor(version_number / 100) % 100,
499 | version_number % 100);
500 | else
501 | buf = sprintf("%d.%d", Math.floor(version_number / 10000),
502 | Math.floor(version_number / 100) % 100);
503 | }
504 | return buf;
505 | }
506 |
507 | /*
508 | * Parse off the next argument for a backslash command, and return it as a
509 | * malloc'd string. If there are no more arguments, returns NULL.
510 | *
511 | * type tells what processing, if any, to perform on the option string;
512 | * for example, if it's a SQL identifier, we want to downcase any unquoted
513 | * letters.
514 | *
515 | * if quote is not NULL, *quote is set to 0 if no quoting was found, else
516 | * the last quote symbol used in the argument.
517 | *
518 | * if semicolon is true, unquoted trailing semicolon(s) that would otherwise
519 | * be taken as part of the option string will be stripped.
520 | *
521 | * NOTE: the only possible syntax errors for backslash options are unmatched
522 | * quotes, which are detected when we run out of input. Therefore, on a
523 | * syntax error we just throw away the string and return NULL; there is no
524 | * need to worry about flushing remaining input.
525 | */
526 | function psql_scan_slash_option(scan_state, type, quote, semicolon) {
527 | if (type !== OT_NORMAL) throw new Error(`scan type ${type} not yet implemented`);
528 | if (quote !== NULL) throw new Error('cannot return quote type');
529 |
530 | const quoteStack = [];
531 | const resultRe = semicolon ? /^(.*);*$/ : /^(.*)$/;
532 | let chr;
533 |
534 | // trim leading whitespace
535 | for (; ;) {
536 | chr = scan_state[0][scan_state[1]]; // => str[index]
537 | if (chr === '\0') return NULL;
538 | if (!isWhitespace(chr)) break;
539 | scan_state[1]++;
540 | }
541 |
542 | // parse for \0 or next unquoted whitespace or \0
543 | let result = '';
544 | while (chr = scan_state[0][scan_state[1]++]) { // => str[index++]
545 | if (chr === '\0') {
546 | if (quoteStack.length > 0) return NULL;
547 | return result.match(resultRe)[1] + '\0';
548 | }
549 |
550 | if (isQuote(chr)) {
551 | if (chr === quoteStack[quoteStack.length - 1]) quoteStack.pop();
552 | else quoteStack.push(chr);
553 | if (chr === '"') result += chr; // ' is not passed through
554 |
555 | } else {
556 | if (quoteStack.length === 0 && isWhitespace(chr)) {
557 | return result.match(resultRe)[1] + '\0';
558 | }
559 | result += chr;
560 | }
561 | }
562 |
563 | return NULL;
564 | }
565 |
566 | function initPQExpBuffer(buf) {
567 | buf.data = '\0';
568 | buf.len = 0;
569 | }
570 | function enlargePQExpBuffer() { return 1; }
571 | function termPQExpBuffer() { }
572 | function PQclear() { }
573 | function free() { }
574 |
575 | function trimTrailingNull(str) {
576 | const nullIndex = str.indexOf('\0');
577 | if (nullIndex !== -1) return str.slice(0, nullIndex);
578 | return str;
579 | }
580 |
581 | function appendPQExpBufferStr(buf, str) {
582 | buf.data = trimTrailingNull(buf.data) + trimTrailingNull(str) + '\0';
583 | buf.len = buf.data.length - 1; // assume (and omit counting) trailing null
584 | }
585 |
586 | function sprintf(template, ...values) {
587 | let result = '';
588 | let valuesIndex = 0;
589 | let chrIndex = 0;
590 | let nextChrIndex;
591 | while ((nextChrIndex = template.indexOf('%', chrIndex)) !== -1) {
592 | result += template.slice(chrIndex, nextChrIndex);
593 | chrIndex = nextChrIndex + 1;
594 | let pcChr = template[chrIndex++];
595 | if (pcChr === '*') pcChr = template[chrIndex++]; // skip a *
596 | if (pcChr === '%') result += '%';
597 | else if (pcChr === 's' || pcChr === 'c' || pcChr === 'd' || pcChr === 'u') result += trimTrailingNull(String(values[valuesIndex++]));
598 | else throw new Error(`Unsupported sprintf placeholder: %${pcChr}`);
599 | }
600 | result += template.slice(chrIndex);
601 | result = trimTrailingNull(result) + '\0';
602 | return result;
603 | }
604 |
605 | function printfPQExpBuffer(buf, template, ...values) {
606 | initPQExpBuffer(buf);
607 | appendPQExpBuffer(buf, template, ...values);
608 | }
609 |
610 | function appendPQExpBuffer(buf, template, ...values) {
611 | const str = sprintf(template, ...values);
612 | appendPQExpBufferStr(buf, str);
613 | }
614 |
615 | function pg_log_error(template, ...args) {
616 | console.error(sprintf(template, ...args));
617 | }
618 |
619 | /*
620 | * validateSQLNamePattern
621 | *
622 | * Wrapper around string_utils's processSQLNamePattern which also checks the
623 | * pattern's validity. In addition to that function's parameters, takes a
624 | * 'maxparts' parameter specifying the maximum number of dotted names the
625 | * pattern is allowed to have, and a 'added_clause' parameter that returns by
626 | * reference whether a clause was added to 'buf'. Returns whether the pattern
627 | * passed validation, after logging any errors.
628 | */
629 | function validateSQLNamePattern(buf, pattern, have_where,
630 | force_escape, schemavar,
631 | namevar, altnamevar,
632 | visibilityrule, added_clause /* bool return param */,
633 | maxparts) {
634 | let dbbuf = { /* struct */ };
635 | let dotcnt = {};
636 | let added;
637 | if (have_where && have_where.value) have_where = have_where.value;
638 |
639 | initPQExpBuffer(dbbuf);
640 | added = processSQLNamePattern(pset.db, buf, pattern, have_where, force_escape,
641 | schemavar, namevar, altnamevar,
642 | visibilityrule, dbbuf, dotcnt);
643 | dotcnt = dotcnt.value;
644 | if (added_clause) added_clause.value = added;
645 |
646 | if (dotcnt >= maxparts) {
647 | pg_log_error("improper qualified name (too many dotted names): %s",
648 | pattern);
649 | return false;
650 | }
651 |
652 | if (maxparts > 1 && dotcnt == maxparts - 1) {
653 | if (PQdb(pset.db) == NULL) {
654 | pg_log_error("You are currently not connected to a database.");
655 | return false;
656 | }
657 | if (strcmp(PQdb(pset.db), dbbuf.data) != 0) {
658 | pg_log_error("cross-database references are not implemented: %s",
659 | pattern);
660 | return false;
661 | }
662 | }
663 | termPQExpBuffer(dbbuf);
664 | return true;
665 | }
666 |
667 | /*
668 | * processSQLNamePattern
669 | *
670 | * Scan a wildcard-pattern string and generate appropriate WHERE clauses
671 | * to limit the set of objects returned. The WHERE clauses are appended
672 | * to the already-partially-constructed query in buf. Returns whether
673 | * any clause was added.
674 | *
675 | * conn: connection query will be sent to (consulted for escaping rules).
676 | * buf: output parameter.
677 | * pattern: user-specified pattern option, or NULL if none ("*" is implied).
678 | * have_where: true if caller already emitted "WHERE" (clauses will be ANDed
679 | * onto the existing WHERE clause).
680 | * force_escape: always quote regexp special characters, even outside
681 | * double quotes (else they are quoted only between double quotes).
682 | * schemavar: name of query variable to match against a schema-name pattern.
683 | * Can be NULL if no schema.
684 | * namevar: name of query variable to match against an object-name pattern.
685 | * altnamevar: NULL, or name of an alternative variable to match against name.
686 | * visibilityrule: clause to use if we want to restrict to visible objects
687 | * (for example, "pg_catalog.pg_table_is_visible(p.oid)"). Can be NULL.
688 | * dbnamebuf: output parameter receiving the database name portion of the
689 | * pattern, if any. Can be NULL.
690 | * dotcnt: how many separators were parsed from the pattern, by reference.
691 | *
692 | * Formatting note: the text already present in buf should end with a newline.
693 | * The appended text, if any, will end with one too.
694 | */
695 | function processSQLNamePattern(conn, buf, pattern,
696 | have_where, force_escape,
697 | schemavar, namevar,
698 | altnamevar, visibilityrule,
699 | dbnamebuf, dotcnt /* integer output param */) {
700 |
701 | let schemabuf = { /* struct */ };
702 | let namebuf = { /* struct */ };
703 | let added_clause = false;
704 |
705 | if (!dotcnt) dotcnt = {};
706 | dotcnt.value = 0;
707 |
708 | if (pattern == NULL) {
709 | /* Default: select all visible objects */
710 | if (visibilityrule) {
711 | appendPQExpBufferStr(buf, have_where ? " AND " : "WHERE "); have_where = true; added_clause = true;
712 | appendPQExpBuffer(buf, "%s\n", visibilityrule);
713 | }
714 | return added_clause;
715 | }
716 |
717 | initPQExpBuffer(schemabuf);
718 | initPQExpBuffer(namebuf);
719 |
720 | /*
721 | * Convert shell-style 'pattern' into the regular expression(s) we want to
722 | * execute. Quoting/escaping into SQL literal format will be done below
723 | * using appendStringLiteralConn().
724 | *
725 | * If the caller provided a schemavar, we want to split the pattern on
726 | * ".", otherwise not.
727 | */
728 | patternToSQLRegex(PQclientEncoding(conn),
729 | (schemavar ? dbnamebuf : NULL),
730 | (schemavar ? schemabuf : NULL),
731 | namebuf,
732 | pattern, force_escape, true, dotcnt);
733 |
734 | /*
735 | * Now decide what we need to emit. We may run under a hostile
736 | * search_path, so qualify EVERY name. Note there will be a leading "^("
737 | * in the patterns in any case.
738 | *
739 | * We want the regex matches to use the database's default collation where
740 | * collation-sensitive behavior is required (for example, which characters
741 | * match '\w'). That happened by default before PG v12, but if the server
742 | * is >= v12 then we need to force it through explicit COLLATE clauses,
743 | * otherwise the "C" collation attached to "name" catalog columns wins.
744 | */
745 | if (namevar && namebuf.len > 2) {
746 | /* We have a name pattern, so constrain the namevar(s) */
747 |
748 | /* Optimize away a "*" pattern */
749 | if (strcmp(namebuf.data, "^(.*)$") != 0) {
750 | appendPQExpBufferStr(buf, have_where ? " AND " : "WHERE "); have_where = true; added_clause = true;
751 | if (altnamevar) {
752 | appendPQExpBuffer(buf,
753 | "(%s OPERATOR(pg_catalog.~) ", namevar);
754 | appendStringLiteralConn(buf, namebuf.data, conn);
755 | if (PQserverVersion(conn) >= 120000)
756 | appendPQExpBufferStr(buf, " COLLATE pg_catalog.default");
757 | appendPQExpBuffer(buf,
758 | "\n OR %s OPERATOR(pg_catalog.~) ",
759 | altnamevar);
760 | appendStringLiteralConn(buf, namebuf.data, conn);
761 | if (PQserverVersion(conn) >= 120000)
762 | appendPQExpBufferStr(buf, " COLLATE pg_catalog.default");
763 | appendPQExpBufferStr(buf, ")\n");
764 | }
765 | else {
766 | appendPQExpBuffer(buf, "%s OPERATOR(pg_catalog.~) ", namevar);
767 | appendStringLiteralConn(buf, namebuf.data, conn);
768 | if (PQserverVersion(conn) >= 120000)
769 | appendPQExpBufferStr(buf, " COLLATE pg_catalog.default");
770 | appendPQExpBufferChar(buf, '\n');
771 | }
772 | }
773 | }
774 |
775 | if (schemavar && schemabuf.len > 2) {
776 | /* We have a schema pattern, so constrain the schemavar */
777 |
778 | /* Optimize away a "*" pattern */
779 | if (strcmp(schemabuf.data, "^(.*)$") != 0 && schemavar) {
780 | appendPQExpBufferStr(buf, have_where ? " AND " : "WHERE "); have_where = true; added_clause = true;
781 | appendPQExpBuffer(buf, "%s OPERATOR(pg_catalog.~) ", schemavar);
782 | appendStringLiteralConn(buf, schemabuf.data, conn);
783 | if (PQserverVersion(conn) >= 120000)
784 | appendPQExpBufferStr(buf, " COLLATE pg_catalog.default");
785 | appendPQExpBufferChar(buf, '\n');
786 | }
787 | }
788 | else {
789 | /* No schema pattern given, so select only visible objects */
790 | if (visibilityrule) {
791 | appendPQExpBufferStr(buf, have_where ? " AND " : "WHERE "); have_where = true; added_clause = true;
792 | appendPQExpBuffer(buf, "%s\n", visibilityrule);
793 | }
794 | }
795 |
796 | termPQExpBuffer(schemabuf);
797 | termPQExpBuffer(namebuf);
798 |
799 | return added_clause;
800 | }
801 |
802 | function appendPQExpBufferChar(str, ch) {
803 | str.data = trimTrailingNull(str.data) + ch + '\0';
804 | str.len++;
805 | }
806 |
807 | /*
808 | * Convert a string value to an SQL string literal and append it to
809 | * the given buffer. We assume the specified client_encoding and
810 | * standard_conforming_strings settings.
811 | *
812 | * This is essentially equivalent to libpq's PQescapeStringInternal,
813 | * except for the output buffer structure. We need it in situations
814 | * where we do not have a PGconn available. Where we do,
815 | * appendStringLiteralConn is a better choice.
816 | */
817 | function appendStringLiteral(buf, str, encoding, std_strings) {
818 | const escaped = str.replace((std_strings ? /[']/g : /['\\]/g), '\\$&');
819 | buf.data = trimTrailingNull(buf.data) + "'" + trimTrailingNull(escaped) + "'\0";
820 | buf.len = buf.data.length - 1;
821 | }
822 |
823 | /*
824 | * Convert a string value to an SQL string literal and append it to
825 | * the given buffer. Encoding and string syntax rules are as indicated
826 | * by current settings of the PGconn.
827 | */
828 | function appendStringLiteralConn(buf, str, conn) {
829 | /*
830 | * XXX This is a kluge to silence escape_string_warning in our utility
831 | * programs. It should go away someday.
832 | */
833 | if (strchr(str, '\\') != NULL && PQserverVersion(conn) >= 80100) {
834 | /* ensure we are not adjacent to an identifier */
835 | if (buf.len > 0 && buf.data[buf.len - 1] != ' ')
836 | appendPQExpBufferChar(buf, ' ');
837 | appendPQExpBufferChar(buf, ESCAPE_STRING_SYNTAX);
838 | appendStringLiteral(buf, str, PQclientEncoding(conn), false);
839 | return;
840 | }
841 | /* XXX end kluge */
842 |
843 | appendStringLiteral(buf, str, conn.encoding, conn.std_strings);
844 | }
845 |
846 | function Assert(cond) {
847 | if (!cond) throw new Error(`Assertion failed (value: ${cond})`);
848 | }
849 |
850 | function isupper(chr) {
851 | const ch = chr.charCodeAt(0);
852 | return ch >= 65 && ch <= 90;
853 | }
854 |
855 | function lengthof(x) {
856 | return x.length;
857 | }
858 |
859 | function pg_tolower(ch) {
860 | return ch.toLowerCase();
861 | }
862 |
863 | function pg_strcasecmp(s1, s2) {
864 | return strcmp(s1.toLowerCase(), s2.toLowerCase());
865 | }
866 |
867 | function exit(exitcode) {
868 | console.error(`Exited (code: ${exitcode})`);
869 | }
870 |
871 | /*
872 | * Transform a possibly qualified shell-style object name pattern into up to
873 | * three SQL-style regular expressions, converting quotes, lower-casing
874 | * unquoted letters, and adjusting shell-style wildcard characters into regexp
875 | * notation.
876 | *
877 | * If the dbnamebuf and schemabuf arguments are non-NULL, and the pattern
878 | * contains two or more dbname/schema/name separators, we parse the portions of
879 | * the pattern prior to the first and second separators into dbnamebuf and
880 | * schemabuf, and the rest into namebuf.
881 | *
882 | * If dbnamebuf is NULL and schemabuf is non-NULL, and the pattern contains at
883 | * least one separator, we parse the first portion into schemabuf and the rest
884 | * into namebuf.
885 | *
886 | * Otherwise, we parse all the pattern into namebuf.
887 | *
888 | * If the pattern contains more dotted parts than buffers to parse into, the
889 | * extra dots will be treated as literal characters and written into the
890 | * namebuf, though they will be counted. Callers should always check the value
891 | * returned by reference in dotcnt and handle this error case appropriately.
892 | *
893 | * We surround the regexps with "^(...)$" to force them to match whole strings,
894 | * as per SQL practice. We have to have parens in case strings contain "|",
895 | * else the "^" and "$" will be bound into the first and last alternatives
896 | * which is not what we want. Whether this is done for dbnamebuf is controlled
897 | * by the want_literal_dbname parameter.
898 | *
899 | * The regexps we parse into the buffers are appended to the data (if any)
900 | * already present. If we parse fewer fields than the number of buffers we
901 | * were given, the extra buffers are unaltered.
902 | *
903 | * encoding: the character encoding for the given pattern
904 | * dbnamebuf: output parameter receiving the database name portion of the
905 | * pattern, if any. Can be NULL.
906 | * schemabuf: output parameter receiving the schema name portion of the
907 | * pattern, if any. Can be NULL.
908 | * namebuf: output parameter receiving the database name portion of the
909 | * pattern, if any. Can be NULL.
910 | * pattern: user-specified pattern option, or NULL if none ("*" is implied).
911 | * force_escape: always quote regexp special characters, even outside
912 | * double quotes (else they are quoted only between double quotes).
913 | * want_literal_dbname: if true, regexp special characters within the database
914 | * name portion of the pattern will not be escaped, nor will the dbname be
915 | * converted into a regular expression.
916 | * dotcnt: output parameter receiving the number of separators parsed from the
917 | * pattern.
918 | */
919 | function patternToSQLRegex(encoding, dbnamebuf /* PQExpBuffer output param */, schemabuf /* PQExpBuffer output param */,
920 | namebuf /* PQExpBuffer output param */, pattern, force_escape,
921 | want_literal_dbname, dotcnt /* int output param */) {
922 | let buf = [{}, {}, {}];
923 | let bufIndex = 0;
924 | let left_literal = {};
925 | let curbuf = {};
926 | let maxbuf = {};
927 | let inquotes;
928 | let left;
929 | let cp;
930 |
931 | Assert(pattern);
932 | Assert(namebuf);
933 |
934 | /* callers should never expect "dbname.relname" format */
935 | Assert(!dbnamebuf || schemabuf);
936 | Assert(dotcnt);
937 |
938 | dotcnt.value = 0;
939 | inquotes = false;
940 | cp = pattern;
941 |
942 | if (dbnamebuf)
943 | maxbuf = 2;
944 | else if (schemabuf)
945 | maxbuf = 1;
946 | else
947 | maxbuf = 0;
948 |
949 | curbuf = buf[bufIndex];
950 | if (want_literal_dbname) {
951 | left = true;
952 | initPQExpBuffer(left_literal);
953 | }
954 | else
955 | left = false;
956 | initPQExpBuffer(curbuf);
957 | appendPQExpBufferStr(curbuf, "^(");
958 |
959 | let cpIndex = 0;
960 | let ch;
961 | while ((ch = cp[cpIndex]) !== '\0') {
962 | if (ch == '"') {
963 | if (inquotes && cp[cpIndex + 1] == '"') {
964 | /* emit one quote, stay in inquotes mode */
965 | appendPQExpBufferChar(curbuf, '"');
966 | if (left)
967 | appendPQExpBufferChar(left_literal, '"');
968 | cpIndex++;
969 | }
970 | else
971 | inquotes = !inquotes;
972 | cpIndex++;
973 | }
974 | else if (!inquotes && isupper(ch)) {
975 | appendPQExpBufferChar(curbuf,
976 | pg_tolower(ch));
977 | if (left)
978 | appendPQExpBufferChar(left_literal,
979 | pg_tolower(ch));
980 | cpIndex++;
981 | }
982 | else if (!inquotes && ch == '*') {
983 | appendPQExpBufferStr(curbuf, ".*");
984 | if (left)
985 | appendPQExpBufferChar(left_literal, '*');
986 | cpIndex++;
987 | }
988 | else if (!inquotes && ch == '?') {
989 | appendPQExpBufferChar(curbuf, '.');
990 | if (left)
991 | appendPQExpBufferChar(left_literal, '?');
992 | cpIndex++;
993 | }
994 | else if (!inquotes && ch == '.') {
995 | left = false;
996 | dotcnt.value++;
997 | if (bufIndex < maxbuf) {
998 | appendPQExpBufferStr(curbuf, ")$");
999 | curbuf = buf[++bufIndex];
1000 | initPQExpBuffer(curbuf);
1001 | appendPQExpBufferStr(curbuf, "^(");
1002 | cpIndex++;
1003 | }
1004 | else {
1005 | appendPQExpBufferChar(curbuf, ch);
1006 | cpIndex++;
1007 | }
1008 | }
1009 | else if (ch == '$') {
1010 | /*
1011 | * Dollar is always quoted, whether inside quotes or not. The
1012 | * reason is that it's allowed in SQL identifiers, so there's a
1013 | * significant use-case for treating it literally, while because
1014 | * we anchor the pattern automatically there is no use-case for
1015 | * having it possess its regexp meaning.
1016 | */
1017 | appendPQExpBufferStr(curbuf, "\\$");
1018 | if (left)
1019 | appendPQExpBufferChar(left_literal, '$');
1020 | cpIndex++;
1021 | }
1022 | else {
1023 | /*
1024 | * Ordinary data character, transfer to pattern
1025 | *
1026 | * Inside double quotes, or at all times if force_escape is true,
1027 | * quote regexp special characters with a backslash to avoid
1028 | * regexp errors. Outside quotes, however, let them pass through
1029 | * as-is; this lets knowledgeable users build regexp expressions
1030 | * that are more powerful than shell-style patterns.
1031 | *
1032 | * As an exception to that, though, always quote "[]", as that's
1033 | * much more likely to be an attempt to write an array type name
1034 | * than it is to be the start of a regexp bracket expression.
1035 | */
1036 | if ((inquotes || force_escape) &&
1037 | strchr("|*+?()[]{}.^$\\", ch))
1038 | appendPQExpBufferChar(curbuf, '\\');
1039 | else if (ch == '[' && cp[cpIndex + 1] == ']')
1040 | appendPQExpBufferChar(curbuf, '\\');
1041 |
1042 | if (left)
1043 | appendPQExpBufferChar(left_literal, ch);
1044 | appendPQExpBufferChar(curbuf, ch);
1045 | cpIndex++;
1046 | }
1047 | }
1048 | appendPQExpBufferStr(curbuf, ")$");
1049 |
1050 | if (namebuf) {
1051 | appendPQExpBufferStr(namebuf, curbuf.data);
1052 | termPQExpBuffer(curbuf);
1053 | curbuf = buf[--bufIndex];
1054 | }
1055 |
1056 | if (schemabuf && bufIndex >= 0) {
1057 | appendPQExpBufferStr(schemabuf, curbuf.data);
1058 | termPQExpBuffer(curbuf);
1059 | curbuf = buf[--bufIndex];
1060 | }
1061 |
1062 | if (dbnamebuf && bufIndex >= 0) {
1063 | if (want_literal_dbname)
1064 | appendPQExpBufferStr(dbnamebuf, left_literal.data);
1065 | else
1066 | appendPQExpBufferStr(dbnamebuf, curbuf.data);
1067 | termPQExpBuffer(curbuf);
1068 | }
1069 |
1070 | if (want_literal_dbname)
1071 | termPQExpBuffer(left_literal);
1072 | }
1073 |
1074 | function column_type_alignment(ftype) {
1075 | let align;
1076 | switch (ftype) {
1077 | case INT2OID:
1078 | case INT4OID:
1079 | case INT8OID:
1080 | case FLOAT4OID:
1081 | case FLOAT8OID:
1082 | case NUMERICOID:
1083 | case OIDOID:
1084 | case XIDOID:
1085 | case XID8OID:
1086 | case CIDOID:
1087 | case MONEYOID:
1088 | align = 'r';
1089 | break;
1090 | default:
1091 | align = 'l';
1092 | break;
1093 | }
1094 | return align;
1095 | }
1096 |
1097 | /*
1098 | * Use this to print query results
1099 | *
1100 | * result: result of a successful query
1101 | * opt: formatting options
1102 | * fout: where to print to
1103 | * is_pager: true if caller has already redirected fout to be a pager pipe
1104 | * flog: if not null, also print the data there (for --log-file option)
1105 | */
1106 | function printQuery(result, opt, fout, is_pager, flog) {
1107 | let cont = {};
1108 | let i, r, c;
1109 |
1110 | printTableInit(cont, opt.topt, opt.title,
1111 | PQnfields(result), PQntuples(result));
1112 |
1113 | /* Assert caller supplied enough translate_columns[] entries */
1114 | Assert(opt.translate_columns == NULL || opt.translate_columns == null ||
1115 | opt.n_translate_columns >= cont.ncolumns);
1116 |
1117 | for (i = 0; i < cont.ncolumns; i++) {
1118 | printTableAddHeader(cont, PQfname(result, i),
1119 | opt.translate_header,
1120 | column_type_alignment(PQftype(result, i)));
1121 | }
1122 |
1123 | /* set cells */
1124 | for (r = 0; r < cont.nrows; r++) {
1125 | for (c = 0; c < cont.ncolumns; c++) {
1126 | let cell;
1127 | let mustfree = false;
1128 | let translate;
1129 |
1130 | if (PQgetisnull(result, r, c))
1131 | cell = opt.nullPrint ? opt.nullPrint : "";
1132 | else {
1133 | cell = PQgetvalue(result, r, c);
1134 | if (cont.aligns[c] == 'r' && opt.topt.numericLocale) {
1135 | cell = format_numeric_locale(cell);
1136 | mustfree = true;
1137 | }
1138 | }
1139 |
1140 | translate = (opt.translate_columns && opt.translate_columns[c]);
1141 | printTableAddCell(cont, cell, translate, mustfree);
1142 | }
1143 | }
1144 |
1145 | /* set footers */
1146 | if (opt.footers) {
1147 | let footer;
1148 | let footerIndex = 0;
1149 | for (footer = opt.footers[footerIndex]; footer; footerIndex++)
1150 | printTableAddFooter(cont, footer);
1151 | }
1152 |
1153 | printTable(cont, fout, is_pager, flog);
1154 | printTableCleanup(cont);
1155 | }
1156 |
1157 | /*
1158 | * Initialise a table contents struct.
1159 | * Must be called before any other printTable method is used.
1160 | *
1161 | * The title is not duplicated; the caller must ensure that the buffer
1162 | * is available for the lifetime of the printTableContent struct.
1163 | *
1164 | * If you call this, you must call printTableCleanup once you're done with the
1165 | * table.
1166 | */
1167 | function printTableInit(content, opt, title, ncolumns, nrows) {
1168 | content.opt = opt;
1169 | content.title = title;
1170 | content.ncolumns = ncolumns;
1171 | content.nrows = nrows;
1172 |
1173 | //content.headerIndex = 0;
1174 | content.headers = []; // pg_malloc0((ncolumns + 1) * sizeof(content.headers));
1175 |
1176 | //content.cellsIndex = 0;
1177 | content.cells = []; // pg_malloc0((ncolumns * nrows + 1) * sizeof(content.cells));
1178 |
1179 | // content.cellmustfree = NULL;
1180 | content.footers = NULL;
1181 |
1182 | // content.alignsIndex = 0;
1183 | content.aligns = []; // pg_malloc0((ncolumns + 1) * sizeof(content.align));
1184 |
1185 | //content.header = content.headers[content.headerIndex];
1186 |
1187 | //content.cell = content.cells[content.cellsIndex];
1188 | //content.footer = content.footers;
1189 | //content.align = content.aligns[content.alignsIndex];
1190 | //content.cellsadded = 0;
1191 | }
1192 |
1193 | function printTableAddHeader(content, header, translate, align) {
1194 | // if (content.headerIndex >= content.ncolumns) {
1195 | // fprintf(stderr, _("Cannot add header to table content: column count of %d exceeded.\n"), content.ncolumns);
1196 | // exit(EXIT_FAILURE);
1197 | // }
1198 |
1199 | if (translate) header = _(header);
1200 | //content.header = content.headers[content.headerIndex] = header;
1201 | //content.headerIndex++;
1202 | content.headers.push(header);
1203 |
1204 | //content.align = content.aligns[content.alignsIndex] = align;
1205 | //content.alignsIndex++;
1206 | content.aligns.push(align);
1207 | }
1208 |
1209 | function printTableAddCell(content, cell, translate, mustfree) {
1210 | // if (content.cellsadded >= content.ncolumns * content.nrows) {
1211 | // fprintf(stderr, _("Cannot add cell to table content: total cell count of %d exceeded.\n"), content.ncolumns * content.nrows);
1212 | // exit(EXIT_FAILURE);
1213 | // }
1214 |
1215 | // if (translate) cell = _(cell);
1216 | // content.cell = content.cells[content.cellsIndex] = cell;
1217 |
1218 | // content.cellsIndex++;
1219 | // content.cellsadded++;
1220 | if (translate) cell = _(cell);
1221 | content.cells.push(cell);
1222 | }
1223 |
1224 | function printTableAddFooter(content, footer) {
1225 | if (content.footers == NULL) content.footers = [];
1226 | content.footers.push(footer);
1227 | }
1228 |
1229 | function stripnulls(obj) {
1230 | if (Array.isArray(obj)) for (let i = 0, len = obj.length; i < len; i++) obj[i] = stripnulls(obj[i]);
1231 | else if (typeof obj === 'object' && obj !== null) for (let i in obj) obj[i] = stripnulls(obj[i]);
1232 | else if (typeof obj === 'string') return trimTrailingNull(obj);
1233 | return obj;
1234 | }
1235 |
1236 | function printTable(cont, fout, is_pager, flog) {
1237 | console.log(JSON.stringify(stripnulls(cont), null, 2));
1238 | }
1239 |
1240 | function printTableCleanup(content) { }
1241 |
1242 |
1243 | import pg from 'pg';
1244 | const raw = x => x;
1245 | for (let b in pg.types.builtins) pg.types.setTypeParser(pg.types.builtins[b], raw);
1246 |
1247 | const
1248 | pool = new pg.Pool({ connectionString: 'postgres://localhost:5435/main' }),
1249 | queryFn = async sql => {
1250 | console.log(`********* QUERY **********\n${sql}\n**************************`);
1251 | const result = await pool.query({ text: sql, rowMode: 'array' });
1252 | // console.log(`********* RESULT **********\n${JSON.stringify(result, null, 2)}\n**************************`);
1253 | return result;
1254 | };
1255 |
1256 | await describe(process.argv[2], 'main', queryFn);
1257 |
1258 | await pool.end();
1259 |
1260 |
--------------------------------------------------------------------------------
/index.d.ts:
--------------------------------------------------------------------------------
1 |
2 | export function describe(
3 | cmd: string,
4 | dbName: string,
5 | runQuery: (sql: string) => any,
6 | outputFn: (item: string | Record) => void,
7 | echoHidden?: boolean,
8 | sversion?: number | null,
9 | std_strings?: boolean,
10 | docsURLTemplate?: (id: string) => string,
11 | ): {
12 | promise: Promise;
13 | cancel: () => void;
14 | };
15 |
16 | export function describeDataToString(item: string | Record): string;
17 | export function describeDataToHtml(item: string | Record): string;
18 |
--------------------------------------------------------------------------------
/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "psql-describe",
3 | "version": "0.1.5",
4 | "lockfileVersion": 3,
5 | "requires": true,
6 | "packages": {
7 | "": {
8 | "name": "psql-describe",
9 | "version": "0.1.5",
10 | "license": "postgresql",
11 | "devDependencies": {
12 | "esbuild": "^0.24.0",
13 | "pg": "^8.11.3"
14 | }
15 | },
16 | "node_modules/@esbuild/aix-ppc64": {
17 | "version": "0.24.0",
18 | "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.0.tgz",
19 | "integrity": "sha512-WtKdFM7ls47zkKHFVzMz8opM7LkcsIp9amDUBIAWirg70RM71WRSjdILPsY5Uv1D42ZpUfaPILDlfactHgsRkw==",
20 | "cpu": [
21 | "ppc64"
22 | ],
23 | "dev": true,
24 | "license": "MIT",
25 | "optional": true,
26 | "os": [
27 | "aix"
28 | ],
29 | "engines": {
30 | "node": ">=18"
31 | }
32 | },
33 | "node_modules/@esbuild/android-arm": {
34 | "version": "0.24.0",
35 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.0.tgz",
36 | "integrity": "sha512-arAtTPo76fJ/ICkXWetLCc9EwEHKaeya4vMrReVlEIUCAUncH7M4bhMQ+M9Vf+FFOZJdTNMXNBrWwW+OXWpSew==",
37 | "cpu": [
38 | "arm"
39 | ],
40 | "dev": true,
41 | "license": "MIT",
42 | "optional": true,
43 | "os": [
44 | "android"
45 | ],
46 | "engines": {
47 | "node": ">=18"
48 | }
49 | },
50 | "node_modules/@esbuild/android-arm64": {
51 | "version": "0.24.0",
52 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.0.tgz",
53 | "integrity": "sha512-Vsm497xFM7tTIPYK9bNTYJyF/lsP590Qc1WxJdlB6ljCbdZKU9SY8i7+Iin4kyhV/KV5J2rOKsBQbB77Ab7L/w==",
54 | "cpu": [
55 | "arm64"
56 | ],
57 | "dev": true,
58 | "license": "MIT",
59 | "optional": true,
60 | "os": [
61 | "android"
62 | ],
63 | "engines": {
64 | "node": ">=18"
65 | }
66 | },
67 | "node_modules/@esbuild/android-x64": {
68 | "version": "0.24.0",
69 | "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.0.tgz",
70 | "integrity": "sha512-t8GrvnFkiIY7pa7mMgJd7p8p8qqYIz1NYiAoKc75Zyv73L3DZW++oYMSHPRarcotTKuSs6m3hTOa5CKHaS02TQ==",
71 | "cpu": [
72 | "x64"
73 | ],
74 | "dev": true,
75 | "license": "MIT",
76 | "optional": true,
77 | "os": [
78 | "android"
79 | ],
80 | "engines": {
81 | "node": ">=18"
82 | }
83 | },
84 | "node_modules/@esbuild/darwin-arm64": {
85 | "version": "0.24.0",
86 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.0.tgz",
87 | "integrity": "sha512-CKyDpRbK1hXwv79soeTJNHb5EiG6ct3efd/FTPdzOWdbZZfGhpbcqIpiD0+vwmpu0wTIL97ZRPZu8vUt46nBSw==",
88 | "cpu": [
89 | "arm64"
90 | ],
91 | "dev": true,
92 | "license": "MIT",
93 | "optional": true,
94 | "os": [
95 | "darwin"
96 | ],
97 | "engines": {
98 | "node": ">=18"
99 | }
100 | },
101 | "node_modules/@esbuild/darwin-x64": {
102 | "version": "0.24.0",
103 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.0.tgz",
104 | "integrity": "sha512-rgtz6flkVkh58od4PwTRqxbKH9cOjaXCMZgWD905JOzjFKW+7EiUObfd/Kav+A6Gyud6WZk9w+xu6QLytdi2OA==",
105 | "cpu": [
106 | "x64"
107 | ],
108 | "dev": true,
109 | "license": "MIT",
110 | "optional": true,
111 | "os": [
112 | "darwin"
113 | ],
114 | "engines": {
115 | "node": ">=18"
116 | }
117 | },
118 | "node_modules/@esbuild/freebsd-arm64": {
119 | "version": "0.24.0",
120 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.0.tgz",
121 | "integrity": "sha512-6Mtdq5nHggwfDNLAHkPlyLBpE5L6hwsuXZX8XNmHno9JuL2+bg2BX5tRkwjyfn6sKbxZTq68suOjgWqCicvPXA==",
122 | "cpu": [
123 | "arm64"
124 | ],
125 | "dev": true,
126 | "license": "MIT",
127 | "optional": true,
128 | "os": [
129 | "freebsd"
130 | ],
131 | "engines": {
132 | "node": ">=18"
133 | }
134 | },
135 | "node_modules/@esbuild/freebsd-x64": {
136 | "version": "0.24.0",
137 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.0.tgz",
138 | "integrity": "sha512-D3H+xh3/zphoX8ck4S2RxKR6gHlHDXXzOf6f/9dbFt/NRBDIE33+cVa49Kil4WUjxMGW0ZIYBYtaGCa2+OsQwQ==",
139 | "cpu": [
140 | "x64"
141 | ],
142 | "dev": true,
143 | "license": "MIT",
144 | "optional": true,
145 | "os": [
146 | "freebsd"
147 | ],
148 | "engines": {
149 | "node": ">=18"
150 | }
151 | },
152 | "node_modules/@esbuild/linux-arm": {
153 | "version": "0.24.0",
154 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.0.tgz",
155 | "integrity": "sha512-gJKIi2IjRo5G6Glxb8d3DzYXlxdEj2NlkixPsqePSZMhLudqPhtZ4BUrpIuTjJYXxvF9njql+vRjB2oaC9XpBw==",
156 | "cpu": [
157 | "arm"
158 | ],
159 | "dev": true,
160 | "license": "MIT",
161 | "optional": true,
162 | "os": [
163 | "linux"
164 | ],
165 | "engines": {
166 | "node": ">=18"
167 | }
168 | },
169 | "node_modules/@esbuild/linux-arm64": {
170 | "version": "0.24.0",
171 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.0.tgz",
172 | "integrity": "sha512-TDijPXTOeE3eaMkRYpcy3LarIg13dS9wWHRdwYRnzlwlA370rNdZqbcp0WTyyV/k2zSxfko52+C7jU5F9Tfj1g==",
173 | "cpu": [
174 | "arm64"
175 | ],
176 | "dev": true,
177 | "license": "MIT",
178 | "optional": true,
179 | "os": [
180 | "linux"
181 | ],
182 | "engines": {
183 | "node": ">=18"
184 | }
185 | },
186 | "node_modules/@esbuild/linux-ia32": {
187 | "version": "0.24.0",
188 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.0.tgz",
189 | "integrity": "sha512-K40ip1LAcA0byL05TbCQ4yJ4swvnbzHscRmUilrmP9Am7//0UjPreh4lpYzvThT2Quw66MhjG//20mrufm40mA==",
190 | "cpu": [
191 | "ia32"
192 | ],
193 | "dev": true,
194 | "license": "MIT",
195 | "optional": true,
196 | "os": [
197 | "linux"
198 | ],
199 | "engines": {
200 | "node": ">=18"
201 | }
202 | },
203 | "node_modules/@esbuild/linux-loong64": {
204 | "version": "0.24.0",
205 | "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.0.tgz",
206 | "integrity": "sha512-0mswrYP/9ai+CU0BzBfPMZ8RVm3RGAN/lmOMgW4aFUSOQBjA31UP8Mr6DDhWSuMwj7jaWOT0p0WoZ6jeHhrD7g==",
207 | "cpu": [
208 | "loong64"
209 | ],
210 | "dev": true,
211 | "license": "MIT",
212 | "optional": true,
213 | "os": [
214 | "linux"
215 | ],
216 | "engines": {
217 | "node": ">=18"
218 | }
219 | },
220 | "node_modules/@esbuild/linux-mips64el": {
221 | "version": "0.24.0",
222 | "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.0.tgz",
223 | "integrity": "sha512-hIKvXm0/3w/5+RDtCJeXqMZGkI2s4oMUGj3/jM0QzhgIASWrGO5/RlzAzm5nNh/awHE0A19h/CvHQe6FaBNrRA==",
224 | "cpu": [
225 | "mips64el"
226 | ],
227 | "dev": true,
228 | "license": "MIT",
229 | "optional": true,
230 | "os": [
231 | "linux"
232 | ],
233 | "engines": {
234 | "node": ">=18"
235 | }
236 | },
237 | "node_modules/@esbuild/linux-ppc64": {
238 | "version": "0.24.0",
239 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.0.tgz",
240 | "integrity": "sha512-HcZh5BNq0aC52UoocJxaKORfFODWXZxtBaaZNuN3PUX3MoDsChsZqopzi5UupRhPHSEHotoiptqikjN/B77mYQ==",
241 | "cpu": [
242 | "ppc64"
243 | ],
244 | "dev": true,
245 | "license": "MIT",
246 | "optional": true,
247 | "os": [
248 | "linux"
249 | ],
250 | "engines": {
251 | "node": ">=18"
252 | }
253 | },
254 | "node_modules/@esbuild/linux-riscv64": {
255 | "version": "0.24.0",
256 | "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.0.tgz",
257 | "integrity": "sha512-bEh7dMn/h3QxeR2KTy1DUszQjUrIHPZKyO6aN1X4BCnhfYhuQqedHaa5MxSQA/06j3GpiIlFGSsy1c7Gf9padw==",
258 | "cpu": [
259 | "riscv64"
260 | ],
261 | "dev": true,
262 | "license": "MIT",
263 | "optional": true,
264 | "os": [
265 | "linux"
266 | ],
267 | "engines": {
268 | "node": ">=18"
269 | }
270 | },
271 | "node_modules/@esbuild/linux-s390x": {
272 | "version": "0.24.0",
273 | "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.0.tgz",
274 | "integrity": "sha512-ZcQ6+qRkw1UcZGPyrCiHHkmBaj9SiCD8Oqd556HldP+QlpUIe2Wgn3ehQGVoPOvZvtHm8HPx+bH20c9pvbkX3g==",
275 | "cpu": [
276 | "s390x"
277 | ],
278 | "dev": true,
279 | "license": "MIT",
280 | "optional": true,
281 | "os": [
282 | "linux"
283 | ],
284 | "engines": {
285 | "node": ">=18"
286 | }
287 | },
288 | "node_modules/@esbuild/linux-x64": {
289 | "version": "0.24.0",
290 | "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.0.tgz",
291 | "integrity": "sha512-vbutsFqQ+foy3wSSbmjBXXIJ6PL3scghJoM8zCL142cGaZKAdCZHyf+Bpu/MmX9zT9Q0zFBVKb36Ma5Fzfa8xA==",
292 | "cpu": [
293 | "x64"
294 | ],
295 | "dev": true,
296 | "license": "MIT",
297 | "optional": true,
298 | "os": [
299 | "linux"
300 | ],
301 | "engines": {
302 | "node": ">=18"
303 | }
304 | },
305 | "node_modules/@esbuild/netbsd-x64": {
306 | "version": "0.24.0",
307 | "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.0.tgz",
308 | "integrity": "sha512-hjQ0R/ulkO8fCYFsG0FZoH+pWgTTDreqpqY7UnQntnaKv95uP5iW3+dChxnx7C3trQQU40S+OgWhUVwCjVFLvg==",
309 | "cpu": [
310 | "x64"
311 | ],
312 | "dev": true,
313 | "license": "MIT",
314 | "optional": true,
315 | "os": [
316 | "netbsd"
317 | ],
318 | "engines": {
319 | "node": ">=18"
320 | }
321 | },
322 | "node_modules/@esbuild/openbsd-arm64": {
323 | "version": "0.24.0",
324 | "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.0.tgz",
325 | "integrity": "sha512-MD9uzzkPQbYehwcN583yx3Tu5M8EIoTD+tUgKF982WYL9Pf5rKy9ltgD0eUgs8pvKnmizxjXZyLt0z6DC3rRXg==",
326 | "cpu": [
327 | "arm64"
328 | ],
329 | "dev": true,
330 | "license": "MIT",
331 | "optional": true,
332 | "os": [
333 | "openbsd"
334 | ],
335 | "engines": {
336 | "node": ">=18"
337 | }
338 | },
339 | "node_modules/@esbuild/openbsd-x64": {
340 | "version": "0.24.0",
341 | "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.0.tgz",
342 | "integrity": "sha512-4ir0aY1NGUhIC1hdoCzr1+5b43mw99uNwVzhIq1OY3QcEwPDO3B7WNXBzaKY5Nsf1+N11i1eOfFcq+D/gOS15Q==",
343 | "cpu": [
344 | "x64"
345 | ],
346 | "dev": true,
347 | "license": "MIT",
348 | "optional": true,
349 | "os": [
350 | "openbsd"
351 | ],
352 | "engines": {
353 | "node": ">=18"
354 | }
355 | },
356 | "node_modules/@esbuild/sunos-x64": {
357 | "version": "0.24.0",
358 | "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.0.tgz",
359 | "integrity": "sha512-jVzdzsbM5xrotH+W5f1s+JtUy1UWgjU0Cf4wMvffTB8m6wP5/kx0KiaLHlbJO+dMgtxKV8RQ/JvtlFcdZ1zCPA==",
360 | "cpu": [
361 | "x64"
362 | ],
363 | "dev": true,
364 | "license": "MIT",
365 | "optional": true,
366 | "os": [
367 | "sunos"
368 | ],
369 | "engines": {
370 | "node": ">=18"
371 | }
372 | },
373 | "node_modules/@esbuild/win32-arm64": {
374 | "version": "0.24.0",
375 | "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.0.tgz",
376 | "integrity": "sha512-iKc8GAslzRpBytO2/aN3d2yb2z8XTVfNV0PjGlCxKo5SgWmNXx82I/Q3aG1tFfS+A2igVCY97TJ8tnYwpUWLCA==",
377 | "cpu": [
378 | "arm64"
379 | ],
380 | "dev": true,
381 | "license": "MIT",
382 | "optional": true,
383 | "os": [
384 | "win32"
385 | ],
386 | "engines": {
387 | "node": ">=18"
388 | }
389 | },
390 | "node_modules/@esbuild/win32-ia32": {
391 | "version": "0.24.0",
392 | "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.0.tgz",
393 | "integrity": "sha512-vQW36KZolfIudCcTnaTpmLQ24Ha1RjygBo39/aLkM2kmjkWmZGEJ5Gn9l5/7tzXA42QGIoWbICfg6KLLkIw6yw==",
394 | "cpu": [
395 | "ia32"
396 | ],
397 | "dev": true,
398 | "license": "MIT",
399 | "optional": true,
400 | "os": [
401 | "win32"
402 | ],
403 | "engines": {
404 | "node": ">=18"
405 | }
406 | },
407 | "node_modules/@esbuild/win32-x64": {
408 | "version": "0.24.0",
409 | "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.0.tgz",
410 | "integrity": "sha512-7IAFPrjSQIJrGsK6flwg7NFmwBoSTyF3rl7If0hNUFQU4ilTsEPL6GuMuU9BfIWVVGuRnuIidkSMC+c0Otu8IA==",
411 | "cpu": [
412 | "x64"
413 | ],
414 | "dev": true,
415 | "license": "MIT",
416 | "optional": true,
417 | "os": [
418 | "win32"
419 | ],
420 | "engines": {
421 | "node": ">=18"
422 | }
423 | },
424 | "node_modules/esbuild": {
425 | "version": "0.24.0",
426 | "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.0.tgz",
427 | "integrity": "sha512-FuLPevChGDshgSicjisSooU0cemp/sGXR841D5LHMB7mTVOmsEHcAxaH3irL53+8YDIeVNQEySh4DaYU/iuPqQ==",
428 | "dev": true,
429 | "hasInstallScript": true,
430 | "license": "MIT",
431 | "bin": {
432 | "esbuild": "bin/esbuild"
433 | },
434 | "engines": {
435 | "node": ">=18"
436 | },
437 | "optionalDependencies": {
438 | "@esbuild/aix-ppc64": "0.24.0",
439 | "@esbuild/android-arm": "0.24.0",
440 | "@esbuild/android-arm64": "0.24.0",
441 | "@esbuild/android-x64": "0.24.0",
442 | "@esbuild/darwin-arm64": "0.24.0",
443 | "@esbuild/darwin-x64": "0.24.0",
444 | "@esbuild/freebsd-arm64": "0.24.0",
445 | "@esbuild/freebsd-x64": "0.24.0",
446 | "@esbuild/linux-arm": "0.24.0",
447 | "@esbuild/linux-arm64": "0.24.0",
448 | "@esbuild/linux-ia32": "0.24.0",
449 | "@esbuild/linux-loong64": "0.24.0",
450 | "@esbuild/linux-mips64el": "0.24.0",
451 | "@esbuild/linux-ppc64": "0.24.0",
452 | "@esbuild/linux-riscv64": "0.24.0",
453 | "@esbuild/linux-s390x": "0.24.0",
454 | "@esbuild/linux-x64": "0.24.0",
455 | "@esbuild/netbsd-x64": "0.24.0",
456 | "@esbuild/openbsd-arm64": "0.24.0",
457 | "@esbuild/openbsd-x64": "0.24.0",
458 | "@esbuild/sunos-x64": "0.24.0",
459 | "@esbuild/win32-arm64": "0.24.0",
460 | "@esbuild/win32-ia32": "0.24.0",
461 | "@esbuild/win32-x64": "0.24.0"
462 | }
463 | },
464 | "node_modules/pg": {
465 | "version": "8.13.1",
466 | "resolved": "https://registry.npmjs.org/pg/-/pg-8.13.1.tgz",
467 | "integrity": "sha512-OUir1A0rPNZlX//c7ksiu7crsGZTKSOXJPgtNiHGIlC9H0lO+NC6ZDYksSgBYY/thSWhnSRBv8w1lieNNGATNQ==",
468 | "dev": true,
469 | "license": "MIT",
470 | "dependencies": {
471 | "pg-connection-string": "^2.7.0",
472 | "pg-pool": "^3.7.0",
473 | "pg-protocol": "^1.7.0",
474 | "pg-types": "^2.1.0",
475 | "pgpass": "1.x"
476 | },
477 | "engines": {
478 | "node": ">= 8.0.0"
479 | },
480 | "optionalDependencies": {
481 | "pg-cloudflare": "^1.1.1"
482 | },
483 | "peerDependencies": {
484 | "pg-native": ">=3.0.1"
485 | },
486 | "peerDependenciesMeta": {
487 | "pg-native": {
488 | "optional": true
489 | }
490 | }
491 | },
492 | "node_modules/pg-cloudflare": {
493 | "version": "1.1.1",
494 | "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz",
495 | "integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==",
496 | "dev": true,
497 | "license": "MIT",
498 | "optional": true
499 | },
500 | "node_modules/pg-connection-string": {
501 | "version": "2.7.0",
502 | "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.7.0.tgz",
503 | "integrity": "sha512-PI2W9mv53rXJQEOb8xNR8lH7Hr+EKa6oJa38zsK0S/ky2er16ios1wLKhZyxzD7jUReiWokc9WK5nxSnC7W1TA==",
504 | "dev": true,
505 | "license": "MIT"
506 | },
507 | "node_modules/pg-int8": {
508 | "version": "1.0.1",
509 | "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
510 | "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
511 | "dev": true,
512 | "license": "ISC",
513 | "engines": {
514 | "node": ">=4.0.0"
515 | }
516 | },
517 | "node_modules/pg-pool": {
518 | "version": "3.7.0",
519 | "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.7.0.tgz",
520 | "integrity": "sha512-ZOBQForurqh4zZWjrgSwwAtzJ7QiRX0ovFkZr2klsen3Nm0aoh33Ls0fzfv3imeH/nw/O27cjdz5kzYJfeGp/g==",
521 | "dev": true,
522 | "license": "MIT",
523 | "peerDependencies": {
524 | "pg": ">=8.0"
525 | }
526 | },
527 | "node_modules/pg-protocol": {
528 | "version": "1.7.0",
529 | "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.7.0.tgz",
530 | "integrity": "sha512-hTK/mE36i8fDDhgDFjy6xNOG+LCorxLG3WO17tku+ij6sVHXh1jQUJ8hYAnRhNla4QVD2H8er/FOjc/+EgC6yQ==",
531 | "dev": true,
532 | "license": "MIT"
533 | },
534 | "node_modules/pg-types": {
535 | "version": "2.2.0",
536 | "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
537 | "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
538 | "dev": true,
539 | "license": "MIT",
540 | "dependencies": {
541 | "pg-int8": "1.0.1",
542 | "postgres-array": "~2.0.0",
543 | "postgres-bytea": "~1.0.0",
544 | "postgres-date": "~1.0.4",
545 | "postgres-interval": "^1.1.0"
546 | },
547 | "engines": {
548 | "node": ">=4"
549 | }
550 | },
551 | "node_modules/pgpass": {
552 | "version": "1.0.5",
553 | "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
554 | "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
555 | "dev": true,
556 | "license": "MIT",
557 | "dependencies": {
558 | "split2": "^4.1.0"
559 | }
560 | },
561 | "node_modules/postgres-array": {
562 | "version": "2.0.0",
563 | "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
564 | "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
565 | "dev": true,
566 | "license": "MIT",
567 | "engines": {
568 | "node": ">=4"
569 | }
570 | },
571 | "node_modules/postgres-bytea": {
572 | "version": "1.0.0",
573 | "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz",
574 | "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==",
575 | "dev": true,
576 | "license": "MIT",
577 | "engines": {
578 | "node": ">=0.10.0"
579 | }
580 | },
581 | "node_modules/postgres-date": {
582 | "version": "1.0.7",
583 | "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
584 | "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
585 | "dev": true,
586 | "license": "MIT",
587 | "engines": {
588 | "node": ">=0.10.0"
589 | }
590 | },
591 | "node_modules/postgres-interval": {
592 | "version": "1.2.0",
593 | "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
594 | "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
595 | "dev": true,
596 | "license": "MIT",
597 | "dependencies": {
598 | "xtend": "^4.0.0"
599 | },
600 | "engines": {
601 | "node": ">=0.10.0"
602 | }
603 | },
604 | "node_modules/split2": {
605 | "version": "4.2.0",
606 | "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
607 | "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
608 | "dev": true,
609 | "license": "ISC",
610 | "engines": {
611 | "node": ">= 10.x"
612 | }
613 | },
614 | "node_modules/xtend": {
615 | "version": "4.0.2",
616 | "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
617 | "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
618 | "dev": true,
619 | "license": "MIT",
620 | "engines": {
621 | "node": ">=0.4"
622 | }
623 | }
624 | }
625 | }
626 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "psql-describe",
3 | "version": "0.1.6",
4 | "description": "psql's `\\d` (describe) family of commands ported to JavaScript.",
5 | "scripts": {
6 | "test": "test/test.mjs /usr/local/pgsql/bin/psql postgres://localhost/psqldescribe < test/tests.txt",
7 | "build": "esbuild src/describe.mjs --outfile=index.js --platform=neutral --target=es2020 --minify --line-limit=120",
8 | "bundleDemo": "esbuild demo-src/demo.js --bundle --outfile=demo/demo.js --platform=neutral --target=es2020"
9 | },
10 | "author": "George MacKerron",
11 | "license": "postgresql",
12 | "devDependencies": {
13 | "esbuild": "^0.24.0",
14 | "pg": "^8.11.3"
15 | },
16 | "repository": {
17 | "type": "git",
18 | "url": "https://github.com/neondatabase/psql-describe"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/test/test-pagila-additions.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE playing_with_neon(id SERIAL PRIMARY KEY, name TEXT NOT NULL, value REAL);
2 | INSERT INTO playing_with_neon(name, value)
3 | SELECT LEFT(md5(i::TEXT), 10), random() FROM generate_series(1, 10) s(i);
4 |
5 | -- extensions with \dx
6 | CREATE EXTENSION citext;
7 | CREATE EXTENSION postgis;
8 |
9 | -- descriptions with \dd
10 | COMMENT ON OPERATOR CLASS char_bloom_ops USING brin IS 'example op class comment';
11 | COMMENT ON TRIGGER last_updated ON staff IS 'example trigger comment';
12 |
13 | -- default privileges with \ddp
14 | CREATE SCHEMA myschema;
15 | ALTER DEFAULT PRIVILEGES IN SCHEMA myschema GRANT SELECT ON TABLES TO PUBLIC;
16 | ALTER DEFAULT PRIVILEGES IN SCHEMA myschema GRANT INSERT ON TABLES TO PUBLIC;
17 |
18 | -- foreign data with \dE, \det
19 | -- echo "1,Bob,1987-12-23T12:01:02.123\n2,Anne,1987-12-23T12:01:02.124" > datadir/test.csv
20 | CREATE EXTENSION file_fdw;
21 | CREATE SERVER csvfile FOREIGN DATA WRAPPER file_fdw;
22 | CREATE FOREIGN TABLE birthdays (
23 | id int,
24 | name text,
25 | birthdate timestamptz
26 | )
27 | SERVER csvfile
28 | OPTIONS ( filename 'test.csv', format 'csv' );
29 |
30 | -- foreign data with \deu
31 | CREATE EXTENSION postgres_fdw;
32 | CREATE SERVER foreign_server
33 | FOREIGN DATA WRAPPER postgres_fdw
34 | OPTIONS (host 'localhost', port '5432', dbname 'psqldescribe');
35 | CREATE USER MAPPING FOR george SERVER foreign_server OPTIONS (user 'bob', password 'secret');
36 |
37 | -- procedures with \dfp
38 | CREATE PROCEDURE myproc(a integer, b integer)
39 | LANGUAGE SQL AS $$
40 | SELECT a + b; -- NB. this is silly because procs don't return anything
41 | $$;
42 |
43 | -- large objects with \dl
44 | SELECT lo_create(0);
45 | SELECT lo_create(0);
46 | SELECT lo_create(0);
47 |
48 | -- partitioned indices with \dPi
49 | CREATE INDEX payments_customer_id_idx ON payment(customer_id);
50 | CREATE INDEX payments_payment_id_idx ON payment(payment_id);
51 |
52 | -- role or DB settings with \drds
53 | ALTER DATABASE psqldescribe SET statement_timeout TO 60000;
54 | CREATE USER myuser;
55 | ALTER ROLE myuser SET statement_timeout TO 30000;
56 |
57 | -- publications with \dRp
58 | CREATE PUBLICATION mypub FOR TABLE actor, film;
59 | CREATE PUBLICATION myotherpub FOR TABLE city, customer;
60 |
61 | -- extended stats with \dX
62 | CREATE STATISTICS estats1 (ndistinct) ON release_year, language_id FROM film;
63 | CREATE STATISTICS estats2 (ndistinct) ON release_year, rating FROM film;
64 |
65 | -- event triggers with \dy
66 | CREATE OR REPLACE FUNCTION do_nothing_on_command() RETURNS event_trigger
67 | LANGUAGE plpgsql AS $$
68 | BEGIN
69 | -- do nothing
70 | END;
71 | $$;
72 | CREATE EVENT TRIGGER do_nothing_ddl ON ddl_command_start EXECUTE FUNCTION do_nothing_on_command();
73 |
--------------------------------------------------------------------------------
/test/test.mjs:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | import pg from 'pg';
4 | import fs from 'fs';
5 | import { describe, describeDataToString } from '../src/describe.mjs';
6 | import { execFileSync, spawn } from 'child_process';
7 |
8 | const [psqlPath, dbUrl, transport] = process.argv.slice(2);
9 | const db = parseURL(dbUrl);
10 | const useHttp = transport === 'http';
11 |
12 | console.log(`
13 | PSQL: ${psqlPath}
14 | DB: postgres://${db.auth !== ':' ? db.auth + '@' : ''}${db.hostname}${db.port ? ':' + db.port : ''}${db.pathname}
15 | Transport: ${useHttp ? 'http' : 'pg'}
16 | `);
17 |
18 | async function spawnAsync(command, args, options) {
19 | // ridiculous shenanigans to work around spawnSync output truncation: https://github.com/nodejs/node/issues/19218
20 | return new Promise(resolve => {
21 | let output = '';
22 | const cmd = spawn(command, args, options);
23 |
24 | cmd.stdin.write(options.input);
25 | cmd.stdin.end();
26 |
27 | cmd.stdout.setEncoding('utf8');
28 | cmd.stdout.on('data', data => output += data);
29 |
30 | cmd.stderr.setEncoding('utf8');
31 | cmd.stderr.on('data', data => output += data);
32 |
33 | cmd.on('close', code => resolve(output));
34 | });
35 | }
36 |
37 | export function parseURL(url, parseQueryString = false) {
38 | const { protocol } = new URL(url);
39 | // we now swap the protocol to http: so that `new URL()` will parse it fully
40 | const httpUrl = 'http:' + url.substring(protocol.length);
41 | let { username, password, host, hostname, port, pathname, search, searchParams, hash } = new URL(httpUrl);
42 | password = decodeURIComponent(password);
43 | const auth = username + ':' + password;
44 | const query = parseQueryString ? Object.fromEntries(searchParams.entries()) : search;
45 | return { href: url, protocol, auth, username, password, host, hostname, port, pathname, search, query, hash };
46 | }
47 |
48 | function psql(input) {
49 | return spawnAsync(psqlPath, [dbUrl, '-E'], {
50 | input,
51 | env: { ...process.env, PGCLIENTENCODING: 'UTF8' }, // to match node-postgres for /dconfig
52 | });
53 | }
54 |
55 | function countLines(str) {
56 | let pos = -1, lineCount = 1;
57 | while ((pos = str.indexOf('\n', pos + 1)) !== -1) lineCount++;
58 | return lineCount;
59 | }
60 |
61 | let queryFn, pool;
62 |
63 | if (useHttp) {
64 | const headers = {
65 | 'Neon-Connection-String': dbUrl,
66 | 'Neon-Raw-Text-Output': 'true', // because we want raw Postgres text format
67 | 'Neon-Array-Mode': 'true', // this saves data and post-processing even if we return objects, not arrays
68 | 'Neon-Pool-Opt-In': 'true',
69 | };
70 | queryFn = async (sql) => {
71 | const response = await fetch('https://proxy.localtest.me:4444/sql', {
72 | method: 'POST',
73 | body: JSON.stringify({ query: sql, params: [] }),
74 | headers,
75 | });
76 | const json = await response.json();
77 | if (response.status === 200) return json;
78 | const msg = json.message.match(/ERROR: (.*?)\n/)[1];
79 | throw new Error(msg);
80 | }
81 |
82 | } else {
83 | pg.types.getTypeParser = () => x => x; // raw pg text format for everything
84 | pool = new pg.Pool({
85 | connectionString: dbUrl,
86 | application_name: 'psql', // to match psql for /dconfig
87 | });
88 | queryFn = sql => pool.query({ text: sql, rowMode: 'array' })
89 | }
90 |
91 | const
92 | testsStr = fs.readFileSync(process.stdin.fd, 'utf-8'),
93 | tests = testsStr.split('\n').map(t => t.trim()).filter(x => !!x);
94 |
95 | for (let test of tests) {
96 | const psqlOutput = await psql(test);
97 |
98 | const localOutputArr = [];
99 | await describe(test, db.pathname.slice(1), queryFn, x => localOutputArr.push(x), true).promise;
100 | const localOutput = localOutputArr.map(x => describeDataToString(x)).join('\n\n');
101 |
102 | const stdPsqlOutput = psqlOutput
103 | .replace(/\/docs\/17\//g, '/docs/current/')
104 | .replace(/ +$/gm, '').trim();
105 |
106 | const stdLocalOutput = localOutput.replace(/ +$/gm, '').trim();
107 |
108 | let pass = stdPsqlOutput === stdLocalOutput;
109 |
110 | // exceptions: ignore unimportant differences
111 | if (!pass) {
112 | if (test === '\\dFp+' &&
113 | stdPsqlOutput.replace(/\n\(\d+ rows?\)/g, '')
114 | === stdLocalOutput.replace(/\n\(\d+ rows?\)/g, '')) {
115 | pass = true;
116 |
117 | } else if ((test === '\\dconfig' || test === '\\dconfig+') &&
118 | stdPsqlOutput.replace(/\n\s*application_name[^\n]+/, '').replace(/\n\(\d+ rows?\)/g, '')
119 | === stdLocalOutput.replace(/\n\(\d+ rows?\)/g, '')) {
120 | pass = true;
121 |
122 | }
123 | }
124 |
125 | const lineCount = countLines(stdLocalOutput);
126 | console.log(pass ? 'Pass' + ' '.repeat(6 - String(lineCount).length) : '*** FAIL ***', lineCount, ' ' + test);
127 |
128 | if (!pass) {
129 | fs.writeFileSync('psql.txt', stdPsqlOutput);
130 | fs.writeFileSync('local.txt', stdLocalOutput);
131 | break;
132 | }
133 | }
134 |
135 | if (!useHttp) await pool.end();
136 |
--------------------------------------------------------------------------------
/test/tests.txt:
--------------------------------------------------------------------------------
1 | \h ALTER DEFAULT PRIVILEGES
2 | \h ALTER DOMAIN
3 | \h ALTER OPERATOR
4 | \h ALTER STATISTICS
5 | \h ALTER TABLE
6 | \h COPY
7 | \h CREATE DATABASE
8 | \h CREATE DOMAIN
9 | \h CREATE ROLE
10 | \h CREATE TABLE
11 | \h DELETE
12 | \h EXPLAIN
13 | \h GRANT
14 | \h INSERT
15 | \h MERGE
16 | \h REVOKE
17 | \h SELECT
18 | \h SELECT INTO
19 | \h TABLE
20 | \h UPDATE
21 | \h WITH
22 | \l
23 | \l+
24 | \l post*
25 | \lo_list
26 | \lo_list+
27 | \z
28 | \z act*
29 | \sf
30 | \sf citext
31 | \sf citext(boolean)
32 | \sf+ citext(boolean)
33 | \sf last_day
34 | \sf+ last_day(timestamptz)
35 | \sv
36 | \sv actor_info
37 | \sv+ actor_info
38 | \sv pg_stats
39 | \sz
40 | \d
41 | \d+
42 | \d actor
43 | \d act*
44 | \d+ act*
45 | \d+ pl*
46 | \d public.Film
47 | \d public."Film"
48 | \d public.'Film'
49 | \d psqldescribe.public.film
50 | \d other.public.film
51 | \dS
52 | \dS+
53 | \dS+ p*
54 | \da
55 | \daS
56 | \daS p*
57 | \dA
58 | \dA+
59 | \dA+ g*
60 | \dA+ public.g*
61 | \dAc
62 | \dAc+
63 | \dAc bri* *id
64 | \dAf
65 | \dAf+
66 | \dAf+ gist ts*
67 | \dAo
68 | \dAo+
69 | \dAo btree
70 | \dAo btree bytea_*
71 | \dAo+ btree bytea_*
72 | \dAp
73 | \dAp+
74 | \dAp brin
75 | \dAp+ brin *_bloom_*
76 | \db
77 | \db+
78 | \db *_default
79 | \db+ *_global
80 | \dc
81 | \dc+
82 | \dcS
83 | \dcS+
84 | \dc *_utf8
85 | \dc+ big5*
86 | \dconfig
87 | \dconfig+
88 | \dconfig data_directory
89 | \dconfig+ wal_buffers
90 | \dC
91 | \dC+
92 | \dC xml
93 | \dC+ text
94 | \dd
95 | \ddS
96 | \dd char*
97 | \dD
98 | \dDS
99 | \dD+
100 | \dDS+ b*
101 | \dDS+ b* hjhj opop
102 | \ddp
103 | \ddp x
104 | \ddp my*
105 | \dE
106 | \dE birthdays
107 | \dE+
108 | \dE x
109 | \di
110 | \diS
111 | \diS+
112 | \di
113 | \di rental_*
114 | \dm
115 | \dm+
116 | \dm *_by_*
117 | \ds
118 | \dsS+
119 | \dt
120 | \dtS
121 | \dt+
122 | \dt c*
123 | \dv
124 | \dvS
125 | \dvS+
126 | \dv+ *or*
127 | \dv+ *or* plpl
128 | \des
129 | \des+
130 | \des csvfile
131 | \det
132 | \det+
133 | \det+ Birthdays
134 | \deu
135 | \deu+
136 | \dew
137 | \dew+
138 | \dew+ file_*
139 | \df
140 | \dfa+
141 | \dfat
142 | \dfn * boolean text
143 | \dfn * boolean -
144 | \dfn * boolean text x
145 | \dfwS+
146 | \dfp
147 | \dfp+
148 | \dfp myproc integer
149 | \dF
150 | \dF+
151 | \dF+ norwegian
152 | \dFd
153 | \dFd+
154 | \dFd+ italian_*
155 | \dFp
156 | \dFp+
157 | \dFt
158 | \dFt+
159 | \dFt+ snow*
160 | \dg
161 | \dgS
162 | \dgS+
163 | \dg p?stgres
164 | \dl
165 | \dl+
166 | \dL
167 | \dL+
168 | \dn
169 | \dn+
170 | \dn+ my*
171 | \do
172 | \doS
173 | \do+
174 | \do ||
175 | \do+ || text text
176 | \dO
177 | \dOS
178 | \dOS+
179 | \dOS+ POSIX
180 | \dOS+ "POSIX"
181 | \dp
182 | \dpS
183 | \dp act*
184 | \dP
185 | \dPn
186 | \dPtn
187 | \dPin
188 | \dPn+ *cust*
189 | \drds
190 | \drds * psqldescribe
191 | \drg
192 | \drgS
193 | \dRp
194 | \dRp+
195 | \dRs
196 | \dRs+
197 | \dT
198 | \dT+
199 | \dTS
200 | \dT+ mpaa*
201 | \du
202 | \duS
203 | \duS+
204 | \du p?stgres
205 | \dx
206 | \dx+
207 | \dx+ ??text
208 | \dX
209 | \dy
210 | \dy+
211 | \dzz
212 |
--------------------------------------------------------------------------------