@yydb/postgresql 0.0.0-stage → 0.0.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/bin/postgres.mjs +17 -0
- package/bin/psql.mjs +236 -0
- package/package.json +59 -4
- package/readme.md +13 -0
- package/src/disguise.ts +23 -0
- package/src/index.ts +1 -0
- package/src/node.ts +1 -0
- package/README.md +0 -3
package/bin/postgres.mjs
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { launchYydsServer } from "@yydb/yyds/node/cluster";
|
|
3
|
+
|
|
4
|
+
const args = process.argv.slice(2);
|
|
5
|
+
if (args.length === 1 && args[0] === "--help") {
|
|
6
|
+
console.log("Usage: postgres --data-dir DIR --cluster-id ID --node-id ID [--port PORT]");
|
|
7
|
+
console.log("Starts the YYDS node and its protocol listener through the yyds cluster tool.");
|
|
8
|
+
} else {
|
|
9
|
+
launchYydsServer("postgresql", args)
|
|
10
|
+
.then((code) => {
|
|
11
|
+
process.exitCode = code;
|
|
12
|
+
})
|
|
13
|
+
.catch((error) => {
|
|
14
|
+
console.error("@yydb/postgresql: " + error.message);
|
|
15
|
+
process.exitCode = 127;
|
|
16
|
+
});
|
|
17
|
+
}
|
package/bin/psql.mjs
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { createConnection } from "node:net";
|
|
4
|
+
|
|
5
|
+
const { version } = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
6
|
+
const maximum = 16 * 1024 * 1024;
|
|
7
|
+
|
|
8
|
+
function message(tag, payload) {
|
|
9
|
+
const header = Buffer.alloc(5);
|
|
10
|
+
header[0] = tag.charCodeAt(0);
|
|
11
|
+
header.writeUInt32BE(payload.length + 4, 1);
|
|
12
|
+
return Buffer.concat([header, payload]);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function argumentsFor(args) {
|
|
16
|
+
const options = {
|
|
17
|
+
host: "127.0.0.1",
|
|
18
|
+
port: 5432,
|
|
19
|
+
user: "yyds",
|
|
20
|
+
database: "yyds",
|
|
21
|
+
sql: "",
|
|
22
|
+
headers: true,
|
|
23
|
+
separator: "|",
|
|
24
|
+
nullValue: "",
|
|
25
|
+
};
|
|
26
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
27
|
+
const option = args[index];
|
|
28
|
+
if (option === "-t" || option === "--tuples-only") {
|
|
29
|
+
options.headers = false;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (option === "-F" || option === "--field-separator") {
|
|
33
|
+
const value = args[++index];
|
|
34
|
+
if (value === undefined || value.includes("\0")) throw new Error(`invalid value for ${option}`);
|
|
35
|
+
options.separator = value;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (option === "-P" || option === "--pset") {
|
|
39
|
+
const value = args[++index];
|
|
40
|
+
if (value === undefined || !value.startsWith("null=")) throw new Error("only psql null output is configurable");
|
|
41
|
+
options.nullValue = value.slice(5);
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const key = {
|
|
45
|
+
"-h": "host",
|
|
46
|
+
"--host": "host",
|
|
47
|
+
"-p": "port",
|
|
48
|
+
"--port": "port",
|
|
49
|
+
"-U": "user",
|
|
50
|
+
"--username": "user",
|
|
51
|
+
"-d": "database",
|
|
52
|
+
"--dbname": "database",
|
|
53
|
+
"-c": "sql",
|
|
54
|
+
"--command": "sql",
|
|
55
|
+
}[option];
|
|
56
|
+
if (!key) throw new Error(`unsupported option '${option}'`);
|
|
57
|
+
const value = args[++index];
|
|
58
|
+
if (value === undefined || value.includes("\0") || Buffer.byteLength(value) > 64 * 1024)
|
|
59
|
+
throw new Error(`invalid value for ${option}`);
|
|
60
|
+
if (key === "port") {
|
|
61
|
+
const port = Number(value);
|
|
62
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535)
|
|
63
|
+
throw new Error("invalid port");
|
|
64
|
+
options.port = port;
|
|
65
|
+
} else options[key] = value;
|
|
66
|
+
}
|
|
67
|
+
if (!options.sql || !options.user || !options.database)
|
|
68
|
+
throw new Error("a nonempty command, user and database are required");
|
|
69
|
+
return options;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function errorResponse(payload) {
|
|
73
|
+
const fields = {};
|
|
74
|
+
let offset = 0;
|
|
75
|
+
while (offset < payload.length && payload[offset] !== 0) {
|
|
76
|
+
const code = String.fromCharCode(payload[offset++]);
|
|
77
|
+
const end = payload.indexOf(0, offset);
|
|
78
|
+
if (end < 0) throw new Error("malformed PostgreSQL error response");
|
|
79
|
+
fields[code] = payload.toString("utf8", offset, end);
|
|
80
|
+
offset = end + 1;
|
|
81
|
+
}
|
|
82
|
+
return new Error(`${fields.C ?? "unknown"}: ${fields.M ?? "PostgreSQL error"}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function columnsFrom(payload) {
|
|
86
|
+
if (payload.length < 2) throw new Error("truncated RowDescription");
|
|
87
|
+
const count = payload.readUInt16BE();
|
|
88
|
+
const names = [];
|
|
89
|
+
let offset = 2;
|
|
90
|
+
for (let index = 0; index < count; index += 1) {
|
|
91
|
+
const end = payload.indexOf(0, offset);
|
|
92
|
+
if (end < 0 || end + 19 > payload.length) throw new Error("truncated column description");
|
|
93
|
+
if (payload.readUInt16BE(end + 17) !== 0)
|
|
94
|
+
throw new Error("binary PostgreSQL results are unsupported");
|
|
95
|
+
names.push(payload.toString("utf8", offset, end));
|
|
96
|
+
offset = end + 19;
|
|
97
|
+
}
|
|
98
|
+
if (offset !== payload.length) throw new Error("trailing RowDescription bytes");
|
|
99
|
+
return names;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function rowFrom(payload, count) {
|
|
103
|
+
if (payload.length < 2 || payload.readUInt16BE() !== count)
|
|
104
|
+
throw new Error("invalid DataRow column count");
|
|
105
|
+
let offset = 2;
|
|
106
|
+
const values = [];
|
|
107
|
+
for (let index = 0; index < count; index += 1) {
|
|
108
|
+
if (offset + 4 > payload.length) throw new Error("truncated DataRow length");
|
|
109
|
+
const length = payload.readInt32BE(offset);
|
|
110
|
+
offset += 4;
|
|
111
|
+
if (length === -1) values.push(null);
|
|
112
|
+
else {
|
|
113
|
+
if (length < 0 || offset + length > payload.length)
|
|
114
|
+
throw new Error("invalid DataRow length");
|
|
115
|
+
values.push(payload.toString("utf8", offset, offset + length));
|
|
116
|
+
offset += length;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (offset !== payload.length) throw new Error("trailing DataRow bytes");
|
|
120
|
+
return values;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function query(options) {
|
|
124
|
+
return new Promise((resolve, reject) => {
|
|
125
|
+
const socket = createConnection({ host: options.host, port: options.port });
|
|
126
|
+
let pending = Buffer.alloc(0);
|
|
127
|
+
let started = false;
|
|
128
|
+
let authenticated = false;
|
|
129
|
+
let currentResult = null;
|
|
130
|
+
let settled = false;
|
|
131
|
+
let received = 0;
|
|
132
|
+
const results = [];
|
|
133
|
+
const timer = setTimeout(() => finish(new Error("PostgreSQL connection timed out")), 5000);
|
|
134
|
+
function finish(error) {
|
|
135
|
+
if (settled) return;
|
|
136
|
+
settled = true;
|
|
137
|
+
clearTimeout(timer);
|
|
138
|
+
socket.destroy();
|
|
139
|
+
if (error) reject(error);
|
|
140
|
+
else resolve(results);
|
|
141
|
+
}
|
|
142
|
+
socket.on("connect", () => {
|
|
143
|
+
const parameters = Buffer.from(
|
|
144
|
+
`user\0${options.user}\0database\0${options.database}\0client_encoding\0UTF8\0application_name\0yyds-psql\0\0`,
|
|
145
|
+
);
|
|
146
|
+
const header = Buffer.alloc(8);
|
|
147
|
+
header.writeUInt32BE(parameters.length + 8);
|
|
148
|
+
header.writeUInt32BE(196608, 4);
|
|
149
|
+
socket.write(Buffer.concat([header, parameters]));
|
|
150
|
+
});
|
|
151
|
+
socket.on("data", (chunk) => {
|
|
152
|
+
try {
|
|
153
|
+
received += chunk.length;
|
|
154
|
+
if (received > maximum) throw new Error("PostgreSQL response exceeds byte limit");
|
|
155
|
+
pending = Buffer.concat([pending, chunk]);
|
|
156
|
+
while (pending.length >= 5) {
|
|
157
|
+
const length = pending.readUInt32BE(1);
|
|
158
|
+
if (length < 4 || length > maximum)
|
|
159
|
+
throw new Error("invalid PostgreSQL message length");
|
|
160
|
+
if (pending.length < length + 1) return;
|
|
161
|
+
const tag = String.fromCharCode(pending[0]);
|
|
162
|
+
const payload = pending.subarray(5, length + 1);
|
|
163
|
+
pending = pending.subarray(length + 1);
|
|
164
|
+
if (tag === "E") throw errorResponse(payload);
|
|
165
|
+
if (tag === "R") {
|
|
166
|
+
if (payload.length !== 4 || payload.readUInt32BE() !== 0)
|
|
167
|
+
throw new Error("PostgreSQL authentication is unsupported");
|
|
168
|
+
authenticated = true;
|
|
169
|
+
} else if (tag === "Z") {
|
|
170
|
+
if (
|
|
171
|
+
!authenticated ||
|
|
172
|
+
payload.length !== 1 ||
|
|
173
|
+
![73, 84, 69].includes(payload[0])
|
|
174
|
+
)
|
|
175
|
+
throw new Error("invalid ReadyForQuery");
|
|
176
|
+
if (started) {
|
|
177
|
+
if (currentResult !== null) throw new Error("PostgreSQL result ended before CommandComplete");
|
|
178
|
+
finish();
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
started = true;
|
|
182
|
+
const sql = Buffer.from(`${options.sql}\0`);
|
|
183
|
+
if (sql.length + 4 > maximum)
|
|
184
|
+
throw new Error("PostgreSQL query exceeds byte limit");
|
|
185
|
+
socket.write(message("Q", sql));
|
|
186
|
+
} else if (tag === "T") {
|
|
187
|
+
if (!started || currentResult !== null) throw new Error("unexpected PostgreSQL RowDescription");
|
|
188
|
+
currentResult = { columns: columnsFrom(payload), rows: [], tag: "" };
|
|
189
|
+
} else if (tag === "D") {
|
|
190
|
+
if (!started || currentResult === null) throw new Error("DataRow without RowDescription");
|
|
191
|
+
currentResult.rows.push(rowFrom(payload, currentResult.columns.length));
|
|
192
|
+
}
|
|
193
|
+
else if (tag === "C") {
|
|
194
|
+
if (!started || payload.length < 2 || payload.indexOf(0) !== payload.length - 1)
|
|
195
|
+
throw new Error("invalid CommandComplete");
|
|
196
|
+
const result = currentResult ?? { columns: [], rows: [], tag: "" };
|
|
197
|
+
result.tag = payload.toString("utf8", 0, payload.length - 1);
|
|
198
|
+
results.push(result);
|
|
199
|
+
currentResult = null;
|
|
200
|
+
} else if (tag === "I") {
|
|
201
|
+
if (!started || payload.length !== 0 || currentResult !== null)
|
|
202
|
+
throw new Error("invalid EmptyQueryResponse");
|
|
203
|
+
} else if (!["S", "K", "N"].includes(tag))
|
|
204
|
+
throw new Error(`unsupported PostgreSQL response '${tag}'`);
|
|
205
|
+
}
|
|
206
|
+
} catch (error) {
|
|
207
|
+
finish(error);
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
socket.on("error", finish);
|
|
211
|
+
socket.on("end", () => finish(new Error("PostgreSQL closed before ReadyForQuery")));
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async function main(args) {
|
|
216
|
+
if (args.length === 1 && ["-V", "--version"].includes(args[0])) {
|
|
217
|
+
console.log(`psql (YYDS ${version})`);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if (args.length === 1 && args[0] === "--help") {
|
|
221
|
+
console.log("Usage: psql [-h HOST] [-p PORT] [-U USER] [-d DATABASE] [-t] [-F SEP] [-P null=TEXT] -c SQL");
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
const options = argumentsFor(args);
|
|
225
|
+
const results = await query(options);
|
|
226
|
+
for (const result of results) {
|
|
227
|
+
if (options.headers && result.columns.length) console.log(result.columns.join(options.separator));
|
|
228
|
+
for (const row of result.rows) console.log(row.map((value) => value ?? options.nullValue).join(options.separator));
|
|
229
|
+
if (result.columns.length === 0 && options.headers) console.log(result.tag);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
main(process.argv.slice(2)).catch((error) => {
|
|
234
|
+
console.error(`@yydb/postgresql: ${error.message}`);
|
|
235
|
+
process.exitCode = 1;
|
|
236
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,61 @@
|
|
|
1
1
|
{
|
|
2
|
+
"bin": {
|
|
3
|
+
"postgres": "./bin/postgres.mjs",
|
|
4
|
+
"psql": "./bin/psql.mjs"
|
|
5
|
+
},
|
|
6
|
+
"bugs": {
|
|
7
|
+
"url": "https://github.com/yy-database/yyds/issues"
|
|
8
|
+
},
|
|
9
|
+
"dependencies": {
|
|
10
|
+
"@yydb/yyds": "0.0.0"
|
|
11
|
+
},
|
|
12
|
+
"description": "Pretend you installed PostgreSQL. YYDS scripting runtime underneath.",
|
|
13
|
+
"devDependencies": {
|
|
14
|
+
"@types/node": "^22.15.0",
|
|
15
|
+
"typescript": "^5.9.2"
|
|
16
|
+
},
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=18"
|
|
19
|
+
},
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"default": "./src/index.ts",
|
|
23
|
+
"import": "./src/index.ts",
|
|
24
|
+
"types": "./src/index.ts"
|
|
25
|
+
},
|
|
26
|
+
"./node": {
|
|
27
|
+
"default": "./src/node.ts",
|
|
28
|
+
"import": "./src/node.ts",
|
|
29
|
+
"types": "./src/node.ts"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"bin",
|
|
34
|
+
"src",
|
|
35
|
+
"readme.md"
|
|
36
|
+
],
|
|
37
|
+
"homepage": "https://github.com/yy-database/yyds/tree/dev/projects/packages/postgresql#readme",
|
|
38
|
+
"keywords": [
|
|
39
|
+
"yyds",
|
|
40
|
+
"postgresql",
|
|
41
|
+
"postgres",
|
|
42
|
+
"disguise"
|
|
43
|
+
],
|
|
44
|
+
"license": "MPL-2.0",
|
|
2
45
|
"name": "@yydb/postgresql",
|
|
3
|
-
"
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"access": "public"
|
|
48
|
+
},
|
|
49
|
+
"repository": {
|
|
50
|
+
"directory": "projects/packages/postgresql",
|
|
51
|
+
"type": "git",
|
|
52
|
+
"url": "git+https://github.com/yy-database/yyds.git"
|
|
53
|
+
},
|
|
54
|
+
"scripts": {
|
|
55
|
+
"test": "node --experimental-strip-types --test \"./tests/**/*.test.ts\"",
|
|
56
|
+
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
57
|
+
},
|
|
58
|
+
"sideEffects": false,
|
|
59
|
+
"type": "module",
|
|
60
|
+
"version": "0.0.0"
|
|
61
|
+
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# @yyds/postgresql
|
|
2
|
+
|
|
3
|
+
`psql` is a PostgreSQL protocol-v3 client for a running YYDS PostgreSQL listener. The listener is started and maintained by the `yyds` tool.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
psql -h 127.0.0.1 -p 5432 -U yyds -d yyds -c "BEGIN"
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
The client supports unauthenticated protocol-v3 startup, text-format simple-query results including multiple result sets, configurable `-F` field separators, configurable `-P null=...` output, tuples-only `-t` output and server errors for one command per invocation. It does not implement TLS, password authentication, binary results, interactive sessions or full psql compatibility. SQL execution is limited to the session commands implemented by the server. Unsupported queries return the server's actual SQLSTATE rather than a fabricated result.
|
|
10
|
+
|
|
11
|
+
`postgres` delegates to the `yyds` executable, including its shared Redis listener, and does not create a PostgreSQL-only supervisor. Pass `--data-dir`, `--cluster-id`, and `--node-id`; set `YYDS_CLI_PATH` when the executable is not on `PATH`.
|
|
12
|
+
|
|
13
|
+
The package API re-exports `@yyds/yyds` unchanged.
|
package/src/disguise.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { loadYydsNative } from "@yydb/yyds/node";
|
|
2
|
+
|
|
3
|
+
export type DisguiseRuntime = {
|
|
4
|
+
product: string;
|
|
5
|
+
yydsVersion: string;
|
|
6
|
+
ping: string;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export function loadDisguiseRuntime(product: string): DisguiseRuntime {
|
|
10
|
+
try {
|
|
11
|
+
const native = loadYydsNative();
|
|
12
|
+
return {
|
|
13
|
+
product,
|
|
14
|
+
yydsVersion: native.yydsVersion(),
|
|
15
|
+
ping: native.ping(),
|
|
16
|
+
};
|
|
17
|
+
} catch (error) {
|
|
18
|
+
throw new Error(
|
|
19
|
+
`@yydb/${product}: native runtime missing. Install @yydb/yyds and run pnpm build:napi`,
|
|
20
|
+
{ cause: error },
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "@yydb/yyds";
|
package/src/node.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "@yydb/yyds/node";
|
package/README.md
DELETED