@volter/world-runtime 3.0.46 → 3.0.48
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/dist/src/pglite-backing.d.ts +5 -0
- package/dist/src/pglite-backing.js +18 -2
- package/dist/src/pglite-host.mjs +46 -28
- package/package.json +4 -4
- package/src/pglite-backing.ts +18 -4
- package/src/pglite-host.mjs +46 -28
|
@@ -2,6 +2,11 @@ export interface InfraService {
|
|
|
2
2
|
kind: string;
|
|
3
3
|
hostPort: number;
|
|
4
4
|
replicaSet?: string;
|
|
5
|
+
postgres?: {
|
|
6
|
+
user: string;
|
|
7
|
+
database: string;
|
|
8
|
+
password?: string;
|
|
9
|
+
};
|
|
5
10
|
}
|
|
6
11
|
/** Read the generated definition's loopback port mappings. MongoDB may explicitly request
|
|
7
12
|
* one replica-set member through its Compose-compatible command; other mongod options are refused. */
|
|
@@ -44,7 +44,20 @@ export function parseInfraDefinition(text) {
|
|
|
44
44
|
});
|
|
45
45
|
if (ports.length !== 1)
|
|
46
46
|
throw new Error(`managed infrastructure ${kind} needs exactly one declared loopback port`);
|
|
47
|
-
|
|
47
|
+
let postgres;
|
|
48
|
+
if (kind === 'postgres') {
|
|
49
|
+
const environment = declared.environment ?? {};
|
|
50
|
+
const user = environment.POSTGRES_USER ?? 'postgres';
|
|
51
|
+
const database = environment.POSTGRES_DB ?? user;
|
|
52
|
+
const password = environment.POSTGRES_PASSWORD;
|
|
53
|
+
if ([user, database, password].some(value => value !== undefined && (typeof value !== 'string' || !value || /[\r\n\0]|\$\{/.test(value)))) {
|
|
54
|
+
throw new Error('World-managed PGlite requires literal POSTGRES_USER, POSTGRES_DB and POSTGRES_PASSWORD');
|
|
55
|
+
}
|
|
56
|
+
if ([user, database].some(value => Buffer.byteLength(value, 'utf8') > 63))
|
|
57
|
+
throw new Error('World-managed PGlite identifiers must fit PostgreSQL\'s 63-byte name limit');
|
|
58
|
+
postgres = { user: user, database: database, ...(password !== undefined ? { password: password } : {}) };
|
|
59
|
+
}
|
|
60
|
+
services.push({ kind, hostPort: ports[0], ...(replicaSet ? { replicaSet } : {}), ...(postgres ? { postgres } : {}) });
|
|
48
61
|
}
|
|
49
62
|
return services;
|
|
50
63
|
}
|
|
@@ -160,6 +173,8 @@ export async function pgliteUp(services, dataDir, launch = {}) {
|
|
|
160
173
|
}
|
|
161
174
|
const name = CONTAINERLESS[service.kind] ?? `pglite-${service.kind}`;
|
|
162
175
|
const serviceData = join(dataDir, containerlessDataDirectory(service.kind) ?? `${name}-data`);
|
|
176
|
+
if (service.kind === 'postgres' && !service.postgres?.password)
|
|
177
|
+
throw new Error('World-managed PGlite requires a literal POSTGRES_PASSWORD; declare a throwaway World credential');
|
|
163
178
|
const log = join(dataDir, `${name}.log`);
|
|
164
179
|
if (await listening(service.hostPort)) {
|
|
165
180
|
throw new Error(`${name} cannot serve port ${service.hostPort}: another process is already listening on it`);
|
|
@@ -177,7 +192,8 @@ export async function pgliteUp(services, dataDir, launch = {}) {
|
|
|
177
192
|
const logFd = openSync(log, 'a');
|
|
178
193
|
const child = spawn(command, args, {
|
|
179
194
|
windowsHide: true,
|
|
180
|
-
env,
|
|
195
|
+
env: service.postgres ? { ...env, VOLTER_WORLD_PGLITE_USER: service.postgres.user,
|
|
196
|
+
VOLTER_WORLD_PGLITE_DATABASE: service.postgres.database, VOLTER_WORLD_PGLITE_PASSWORD: service.postgres.password } : env,
|
|
181
197
|
detached: true,
|
|
182
198
|
stdio: ['ignore', logFd, logFd],
|
|
183
199
|
});
|
package/dist/src/pglite-host.mjs
CHANGED
|
@@ -33,18 +33,11 @@
|
|
|
33
33
|
//
|
|
34
34
|
// Limits, stated plainly (measured on 0.5.8 through this gateway with `pg`
|
|
35
35
|
// clients; pglite-backing.test.ts holds the extension and database checks):
|
|
36
|
-
// - ONE database
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
// nothing can ever connect to it, so this host fails the statement loudly
|
|
42
|
-
// (see refusal() below): 42P04 "already exists" when the name is the
|
|
43
|
-
// connection's own startup database (true: it connects, so check-then-
|
|
44
|
-
// create for the app's own database proceeds), 0A000 "not supported" for
|
|
45
|
-
// any other name. An app that needs a second real database (Prisma's
|
|
46
|
-
// shadow database for `migrate dev`, Django's `test_<name>`; `migrate
|
|
47
|
-
// deploy` needs none) needs the container backing.
|
|
36
|
+
// - ONE declared database and login, authenticated with the World's throwaway
|
|
37
|
+
// password. Undeclared databases fail 3D000; failed logins fail 28P01.
|
|
38
|
+
// current_database() and current_user come from the configured backend.
|
|
39
|
+
// CREATE DATABASE of the declared name fails 42P04, other names fail 0A000;
|
|
40
|
+
// multiple databases require the native or container backing.
|
|
48
41
|
// - No COPY … FROM STDIN: refused with 0A000 (it aborts PGlite's backend);
|
|
49
42
|
// COPY … TO STDOUT works.
|
|
50
43
|
// - ONE serialized session. Connections are accepted concurrently but run
|
|
@@ -96,6 +89,7 @@ import net from 'node:net';
|
|
|
96
89
|
import { mkdirSync, readFileSync } from 'node:fs';
|
|
97
90
|
import clockForm from '@volter/world-core/world-clock';
|
|
98
91
|
import { fromNodeSocket } from 'pg-gateway/node';
|
|
92
|
+
import { BackendError } from 'pg-gateway';
|
|
99
93
|
|
|
100
94
|
// ---- the World's time (above) --------------------------------
|
|
101
95
|
const machineNow = Date.now.bind(Date);
|
|
@@ -147,6 +141,13 @@ function arg(name) {
|
|
|
147
141
|
|
|
148
142
|
const port = Number(arg('--port'));
|
|
149
143
|
const dataDir = arg('--data');
|
|
144
|
+
const databaseUser = process.env.VOLTER_WORLD_PGLITE_USER;
|
|
145
|
+
const databaseName = process.env.VOLTER_WORLD_PGLITE_DATABASE;
|
|
146
|
+
const databasePassword = process.env.VOLTER_WORLD_PGLITE_PASSWORD;
|
|
147
|
+
if (![databaseUser, databaseName, databasePassword].every(value => typeof value === 'string' && value.length > 0)) {
|
|
148
|
+
process.stderr.write('pglite-host: declared database identity and throwaway password are required\n');
|
|
149
|
+
process.exit(2);
|
|
150
|
+
}
|
|
150
151
|
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
151
152
|
process.stderr.write('pglite-host: --port must be 1-65535\n');
|
|
152
153
|
process.exit(2);
|
|
@@ -159,7 +160,27 @@ process.stderr.on('error', () => {});
|
|
|
159
160
|
// PGlite's create() only progresses while something holds the event loop;
|
|
160
161
|
// listening starts AFTER readiness so "port answers" means "serves queries"
|
|
161
162
|
const initKeepAlive = setInterval(() => {}, 500);
|
|
162
|
-
const
|
|
163
|
+
const quoteIdentifier = value => `"${value.replaceAll('"', '""')}"`;
|
|
164
|
+
async function openDeclaredDatabase() {
|
|
165
|
+
// PostgreSQL cannot rename its current database. Use its maintenance database
|
|
166
|
+
// before opening the declared one; old Worlds' rows in postgres stay intact.
|
|
167
|
+
const maintenance = await PGlite.create({ dataDir, extensions, database: 'template1' });
|
|
168
|
+
try {
|
|
169
|
+
const existing = await maintenance.query('SELECT datname FROM pg_database WHERE datname = $1', [databaseName]);
|
|
170
|
+
if (!existing.rows.length) {
|
|
171
|
+
const legacy = await maintenance.query("SELECT datname FROM pg_database WHERE datname = 'postgres'");
|
|
172
|
+
await maintenance.exec(legacy.rows.length
|
|
173
|
+
? `ALTER DATABASE postgres RENAME TO ${quoteIdentifier(databaseName)}`
|
|
174
|
+
: `CREATE DATABASE ${quoteIdentifier(databaseName)}`);
|
|
175
|
+
}
|
|
176
|
+
const role = await maintenance.query('SELECT rolname FROM pg_roles WHERE rolname = $1', [databaseUser]);
|
|
177
|
+
if (!role.rows.length) await maintenance.exec(`CREATE ROLE ${quoteIdentifier(databaseUser)} LOGIN SUPERUSER`);
|
|
178
|
+
} finally { await maintenance.close(); }
|
|
179
|
+
const db = await PGlite.create({ dataDir, extensions, database: databaseName });
|
|
180
|
+
await db.exec(`SET SESSION AUTHORIZATION ${quoteIdentifier(databaseUser)}`);
|
|
181
|
+
return db;
|
|
182
|
+
}
|
|
183
|
+
const dbReady = openDeclaredDatabase();
|
|
163
184
|
dbReady.catch((error) => {
|
|
164
185
|
process.stderr.write(`pglite-host: backend failed to start: ${errorMessage(error)}\n`);
|
|
165
186
|
process.exit(1);
|
|
@@ -624,17 +645,9 @@ function lastReadyStatus(response) {
|
|
|
624
645
|
}
|
|
625
646
|
|
|
626
647
|
// ---- CREATE DATABASE refusal -----------------------------------------------
|
|
627
|
-
//
|
|
628
|
-
//
|
|
629
|
-
//
|
|
630
|
-
// in the one real database (a Prisma shadow database would replay migrations
|
|
631
|
-
// into it; a test runner would treat it as disposable). So the statement fails:
|
|
632
|
-
// - naming the connection's OWN startup database (or `postgres`), with 42P04
|
|
633
|
-
// duplicate_database — true, since that name connects, and the answer
|
|
634
|
-
// check-then-create setup (`rails db:create`) tolerates;
|
|
635
|
-
// - naming any other database, with 0A000 feature_not_supported, because a
|
|
636
|
-
// tool that treats 42P04 as "reuse it" would then connect to the alias of
|
|
637
|
-
// the real database and treat it as disposable.
|
|
648
|
+
// Only the declared database can be connected to. Creating another catalog
|
|
649
|
+
// row would advertise a database this service cannot serve, so the statement
|
|
650
|
+
// fails: 42P04 for the declared name, 0A000 for another name.
|
|
638
651
|
// The statement text is rewritten to a DO block that raises, so the BACKEND
|
|
639
652
|
// reports the error and owns the protocol and transaction state
|
|
640
653
|
// (ErrorResponse, aborted block, ReadyForQuery) as for any failing statement.
|
|
@@ -662,9 +675,9 @@ const raising = (errcode, message, detail, hint) => `DO ${DOLLAR_TAG} BEGIN RAIS
|
|
|
662
675
|
* `own`, in plain SQL literal quoting. */
|
|
663
676
|
function refusal(name, own) {
|
|
664
677
|
const label = name && !name.includes(DOLLAR_TAG) ? ` "${name}"` : '';
|
|
665
|
-
const exists = name !== undefined &&
|
|
666
|
-
const detail = "The World's PGlite backing serves ONE database;
|
|
667
|
-
const hint = 'Use the database named in DATABASE_URL, or
|
|
678
|
+
const exists = name !== undefined && name === own;
|
|
679
|
+
const detail = "The World's PGlite backing serves ONE database; only its declared database can be connected to, so CREATE DATABASE cannot make a separately served one.";
|
|
680
|
+
const hint = 'Use the database named in DATABASE_URL, or select the native PostgreSQL or container backing for multiple databases.';
|
|
668
681
|
return exists
|
|
669
682
|
? raising('duplicate_database', `database${label} already exists`, detail, hint)
|
|
670
683
|
: raising('feature_not_supported', `CREATE DATABASE${label} is not supported: this World serves one database`, detail, hint);
|
|
@@ -785,7 +798,12 @@ const server = net.createServer(async (socket) => {
|
|
|
785
798
|
socket.on('error', () => { /* close handler owns cleanup */ });
|
|
786
799
|
await fromNodeSocket(socket, {
|
|
787
800
|
serverVersion: '18.3 (PGlite)',
|
|
788
|
-
auth: { method: '
|
|
801
|
+
auth: { method: 'password', getClearTextPassword: () => databasePassword,
|
|
802
|
+
validateCredentials: ({ username, password }) => username === databaseUser && password === databasePassword },
|
|
803
|
+
onStartup({ clientParams }) {
|
|
804
|
+
const requested = clientParams?.database || clientParams?.user;
|
|
805
|
+
if (requested !== databaseName) throw BackendError.create({ severity: 'FATAL', code: '3D000', message: `database "${requested}" does not exist` });
|
|
806
|
+
},
|
|
789
807
|
async onMessage(data, { isAuthenticated, clientParams }) {
|
|
790
808
|
if (!isAuthenticated) return undefined; // gateway owns startup/auth traffic
|
|
791
809
|
const client = data;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/world-runtime",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.48",
|
|
4
4
|
"description": "World configs for twins: boot named local runtimes, allocate ports, generate world.env/instance.json, and run apps against fake-key twin worlds.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"twin",
|
|
@@ -69,14 +69,14 @@
|
|
|
69
69
|
"node": ">=22.3"
|
|
70
70
|
},
|
|
71
71
|
"peerDependencies": {
|
|
72
|
-
"@volter/world-core": "3.0.
|
|
73
|
-
"@volter/world-console": "3.0.
|
|
72
|
+
"@volter/world-core": "3.0.48",
|
|
73
|
+
"@volter/world-console": "3.0.43"
|
|
74
74
|
},
|
|
75
75
|
"dependencies": {
|
|
76
76
|
"@electric-sql/pglite": "0.5.8",
|
|
77
77
|
"@electric-sql/pglite-pgvector": "0.0.9",
|
|
78
78
|
"@volter/world-access": "3.0.1",
|
|
79
|
-
"@volter/world-core": "3.0.
|
|
79
|
+
"@volter/world-core": "3.0.48",
|
|
80
80
|
"mongodb": "~6.20.0",
|
|
81
81
|
"pg-gateway": "0.3.0-beta.4",
|
|
82
82
|
"smol-toml": "^1.8.0",
|
package/src/pglite-backing.ts
CHANGED
|
@@ -23,7 +23,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
23
23
|
import { parse } from 'yaml';
|
|
24
24
|
import { mongodbArgv, mongodbListening, mongodbReady } from './mongodb-backing.ts';
|
|
25
25
|
|
|
26
|
-
export interface InfraService { kind: string; hostPort: number; replicaSet?: string }
|
|
26
|
+
export interface InfraService { kind: string; hostPort: number; replicaSet?: string; postgres?: { user: string; database: string; password?: string } }
|
|
27
27
|
|
|
28
28
|
/** Read the generated definition's loopback port mappings. MongoDB may explicitly request
|
|
29
29
|
* one replica-set member through its Compose-compatible command; other mongod options are refused. */
|
|
@@ -31,7 +31,7 @@ export function parseInfraDefinition(text: string): InfraService[] {
|
|
|
31
31
|
const definition = parse(text);
|
|
32
32
|
const services: InfraService[] = [];
|
|
33
33
|
for (const [kind, value] of Object.entries(definition?.services ?? {})) {
|
|
34
|
-
const declared = value as { ports?: unknown[]; command?: unknown };
|
|
34
|
+
const declared = value as { ports?: unknown[]; command?: unknown; environment?: Record<string, unknown> };
|
|
35
35
|
let replicaSet: string | undefined;
|
|
36
36
|
if (kind === 'mongodb' && declared.command !== undefined) {
|
|
37
37
|
const command = declared.command;
|
|
@@ -46,7 +46,19 @@ export function parseInfraDefinition(text: string): InfraService[] {
|
|
|
46
46
|
return match ? [Number(match[1])] : [];
|
|
47
47
|
});
|
|
48
48
|
if (ports.length !== 1) throw new Error(`managed infrastructure ${kind} needs exactly one declared loopback port`);
|
|
49
|
-
|
|
49
|
+
let postgres: InfraService['postgres'];
|
|
50
|
+
if (kind === 'postgres') {
|
|
51
|
+
const environment = declared.environment ?? {};
|
|
52
|
+
const user = environment.POSTGRES_USER ?? 'postgres';
|
|
53
|
+
const database = environment.POSTGRES_DB ?? user;
|
|
54
|
+
const password = environment.POSTGRES_PASSWORD;
|
|
55
|
+
if ([user, database, password].some(value => value !== undefined && (typeof value !== 'string' || !value || /[\r\n\0]|\$\{/.test(value)))) {
|
|
56
|
+
throw new Error('World-managed PGlite requires literal POSTGRES_USER, POSTGRES_DB and POSTGRES_PASSWORD');
|
|
57
|
+
}
|
|
58
|
+
if ([user, database].some(value => Buffer.byteLength(value as string, 'utf8') > 63)) throw new Error('World-managed PGlite identifiers must fit PostgreSQL\'s 63-byte name limit');
|
|
59
|
+
postgres = { user: user as string, database: database as string, ...(password !== undefined ? { password: password as string } : {}) };
|
|
60
|
+
}
|
|
61
|
+
services.push({ kind, hostPort: ports[0]!, ...(replicaSet ? { replicaSet } : {}), ...(postgres ? { postgres } : {}) });
|
|
50
62
|
}
|
|
51
63
|
return services;
|
|
52
64
|
}
|
|
@@ -167,6 +179,7 @@ export async function pgliteUp(services: InfraService[], dataDir: string, launch
|
|
|
167
179
|
}
|
|
168
180
|
const name = CONTAINERLESS[service.kind] ?? `pglite-${service.kind}`;
|
|
169
181
|
const serviceData = join(dataDir, containerlessDataDirectory(service.kind) ?? `${name}-data`);
|
|
182
|
+
if (service.kind === 'postgres' && !service.postgres?.password) throw new Error('World-managed PGlite requires a literal POSTGRES_PASSWORD; declare a throwaway World credential');
|
|
170
183
|
const log = join(dataDir, `${name}.log`);
|
|
171
184
|
if (await listening(service.hostPort)) {
|
|
172
185
|
throw new Error(`${name} cannot serve port ${service.hostPort}: another process is already listening on it`);
|
|
@@ -184,7 +197,8 @@ export async function pgliteUp(services: InfraService[], dataDir: string, launch
|
|
|
184
197
|
const logFd = openSync(log, 'a');
|
|
185
198
|
const child = spawn(command!, args, {
|
|
186
199
|
windowsHide: true,
|
|
187
|
-
env,
|
|
200
|
+
env: service.postgres ? { ...env, VOLTER_WORLD_PGLITE_USER: service.postgres.user,
|
|
201
|
+
VOLTER_WORLD_PGLITE_DATABASE: service.postgres.database, VOLTER_WORLD_PGLITE_PASSWORD: service.postgres.password } : env,
|
|
188
202
|
detached: true,
|
|
189
203
|
stdio: ['ignore', logFd, logFd],
|
|
190
204
|
});
|
package/src/pglite-host.mjs
CHANGED
|
@@ -33,18 +33,11 @@
|
|
|
33
33
|
//
|
|
34
34
|
// Limits, stated plainly (measured on 0.5.8 through this gateway with `pg`
|
|
35
35
|
// clients; pglite-backing.test.ts holds the extension and database checks):
|
|
36
|
-
// - ONE database
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
// nothing can ever connect to it, so this host fails the statement loudly
|
|
42
|
-
// (see refusal() below): 42P04 "already exists" when the name is the
|
|
43
|
-
// connection's own startup database (true: it connects, so check-then-
|
|
44
|
-
// create for the app's own database proceeds), 0A000 "not supported" for
|
|
45
|
-
// any other name. An app that needs a second real database (Prisma's
|
|
46
|
-
// shadow database for `migrate dev`, Django's `test_<name>`; `migrate
|
|
47
|
-
// deploy` needs none) needs the container backing.
|
|
36
|
+
// - ONE declared database and login, authenticated with the World's throwaway
|
|
37
|
+
// password. Undeclared databases fail 3D000; failed logins fail 28P01.
|
|
38
|
+
// current_database() and current_user come from the configured backend.
|
|
39
|
+
// CREATE DATABASE of the declared name fails 42P04, other names fail 0A000;
|
|
40
|
+
// multiple databases require the native or container backing.
|
|
48
41
|
// - No COPY … FROM STDIN: refused with 0A000 (it aborts PGlite's backend);
|
|
49
42
|
// COPY … TO STDOUT works.
|
|
50
43
|
// - ONE serialized session. Connections are accepted concurrently but run
|
|
@@ -96,6 +89,7 @@ import net from 'node:net';
|
|
|
96
89
|
import { mkdirSync, readFileSync } from 'node:fs';
|
|
97
90
|
import clockForm from '@volter/world-core/world-clock';
|
|
98
91
|
import { fromNodeSocket } from 'pg-gateway/node';
|
|
92
|
+
import { BackendError } from 'pg-gateway';
|
|
99
93
|
|
|
100
94
|
// ---- the World's time (above) --------------------------------
|
|
101
95
|
const machineNow = Date.now.bind(Date);
|
|
@@ -147,6 +141,13 @@ function arg(name) {
|
|
|
147
141
|
|
|
148
142
|
const port = Number(arg('--port'));
|
|
149
143
|
const dataDir = arg('--data');
|
|
144
|
+
const databaseUser = process.env.VOLTER_WORLD_PGLITE_USER;
|
|
145
|
+
const databaseName = process.env.VOLTER_WORLD_PGLITE_DATABASE;
|
|
146
|
+
const databasePassword = process.env.VOLTER_WORLD_PGLITE_PASSWORD;
|
|
147
|
+
if (![databaseUser, databaseName, databasePassword].every(value => typeof value === 'string' && value.length > 0)) {
|
|
148
|
+
process.stderr.write('pglite-host: declared database identity and throwaway password are required\n');
|
|
149
|
+
process.exit(2);
|
|
150
|
+
}
|
|
150
151
|
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
151
152
|
process.stderr.write('pglite-host: --port must be 1-65535\n');
|
|
152
153
|
process.exit(2);
|
|
@@ -159,7 +160,27 @@ process.stderr.on('error', () => {});
|
|
|
159
160
|
// PGlite's create() only progresses while something holds the event loop;
|
|
160
161
|
// listening starts AFTER readiness so "port answers" means "serves queries"
|
|
161
162
|
const initKeepAlive = setInterval(() => {}, 500);
|
|
162
|
-
const
|
|
163
|
+
const quoteIdentifier = value => `"${value.replaceAll('"', '""')}"`;
|
|
164
|
+
async function openDeclaredDatabase() {
|
|
165
|
+
// PostgreSQL cannot rename its current database. Use its maintenance database
|
|
166
|
+
// before opening the declared one; old Worlds' rows in postgres stay intact.
|
|
167
|
+
const maintenance = await PGlite.create({ dataDir, extensions, database: 'template1' });
|
|
168
|
+
try {
|
|
169
|
+
const existing = await maintenance.query('SELECT datname FROM pg_database WHERE datname = $1', [databaseName]);
|
|
170
|
+
if (!existing.rows.length) {
|
|
171
|
+
const legacy = await maintenance.query("SELECT datname FROM pg_database WHERE datname = 'postgres'");
|
|
172
|
+
await maintenance.exec(legacy.rows.length
|
|
173
|
+
? `ALTER DATABASE postgres RENAME TO ${quoteIdentifier(databaseName)}`
|
|
174
|
+
: `CREATE DATABASE ${quoteIdentifier(databaseName)}`);
|
|
175
|
+
}
|
|
176
|
+
const role = await maintenance.query('SELECT rolname FROM pg_roles WHERE rolname = $1', [databaseUser]);
|
|
177
|
+
if (!role.rows.length) await maintenance.exec(`CREATE ROLE ${quoteIdentifier(databaseUser)} LOGIN SUPERUSER`);
|
|
178
|
+
} finally { await maintenance.close(); }
|
|
179
|
+
const db = await PGlite.create({ dataDir, extensions, database: databaseName });
|
|
180
|
+
await db.exec(`SET SESSION AUTHORIZATION ${quoteIdentifier(databaseUser)}`);
|
|
181
|
+
return db;
|
|
182
|
+
}
|
|
183
|
+
const dbReady = openDeclaredDatabase();
|
|
163
184
|
dbReady.catch((error) => {
|
|
164
185
|
process.stderr.write(`pglite-host: backend failed to start: ${errorMessage(error)}\n`);
|
|
165
186
|
process.exit(1);
|
|
@@ -624,17 +645,9 @@ function lastReadyStatus(response) {
|
|
|
624
645
|
}
|
|
625
646
|
|
|
626
647
|
// ---- CREATE DATABASE refusal -----------------------------------------------
|
|
627
|
-
//
|
|
628
|
-
//
|
|
629
|
-
//
|
|
630
|
-
// in the one real database (a Prisma shadow database would replay migrations
|
|
631
|
-
// into it; a test runner would treat it as disposable). So the statement fails:
|
|
632
|
-
// - naming the connection's OWN startup database (or `postgres`), with 42P04
|
|
633
|
-
// duplicate_database — true, since that name connects, and the answer
|
|
634
|
-
// check-then-create setup (`rails db:create`) tolerates;
|
|
635
|
-
// - naming any other database, with 0A000 feature_not_supported, because a
|
|
636
|
-
// tool that treats 42P04 as "reuse it" would then connect to the alias of
|
|
637
|
-
// the real database and treat it as disposable.
|
|
648
|
+
// Only the declared database can be connected to. Creating another catalog
|
|
649
|
+
// row would advertise a database this service cannot serve, so the statement
|
|
650
|
+
// fails: 42P04 for the declared name, 0A000 for another name.
|
|
638
651
|
// The statement text is rewritten to a DO block that raises, so the BACKEND
|
|
639
652
|
// reports the error and owns the protocol and transaction state
|
|
640
653
|
// (ErrorResponse, aborted block, ReadyForQuery) as for any failing statement.
|
|
@@ -662,9 +675,9 @@ const raising = (errcode, message, detail, hint) => `DO ${DOLLAR_TAG} BEGIN RAIS
|
|
|
662
675
|
* `own`, in plain SQL literal quoting. */
|
|
663
676
|
function refusal(name, own) {
|
|
664
677
|
const label = name && !name.includes(DOLLAR_TAG) ? ` "${name}"` : '';
|
|
665
|
-
const exists = name !== undefined &&
|
|
666
|
-
const detail = "The World's PGlite backing serves ONE database;
|
|
667
|
-
const hint = 'Use the database named in DATABASE_URL, or
|
|
678
|
+
const exists = name !== undefined && name === own;
|
|
679
|
+
const detail = "The World's PGlite backing serves ONE database; only its declared database can be connected to, so CREATE DATABASE cannot make a separately served one.";
|
|
680
|
+
const hint = 'Use the database named in DATABASE_URL, or select the native PostgreSQL or container backing for multiple databases.';
|
|
668
681
|
return exists
|
|
669
682
|
? raising('duplicate_database', `database${label} already exists`, detail, hint)
|
|
670
683
|
: raising('feature_not_supported', `CREATE DATABASE${label} is not supported: this World serves one database`, detail, hint);
|
|
@@ -785,7 +798,12 @@ const server = net.createServer(async (socket) => {
|
|
|
785
798
|
socket.on('error', () => { /* close handler owns cleanup */ });
|
|
786
799
|
await fromNodeSocket(socket, {
|
|
787
800
|
serverVersion: '18.3 (PGlite)',
|
|
788
|
-
auth: { method: '
|
|
801
|
+
auth: { method: 'password', getClearTextPassword: () => databasePassword,
|
|
802
|
+
validateCredentials: ({ username, password }) => username === databaseUser && password === databasePassword },
|
|
803
|
+
onStartup({ clientParams }) {
|
|
804
|
+
const requested = clientParams?.database || clientParams?.user;
|
|
805
|
+
if (requested !== databaseName) throw BackendError.create({ severity: 'FATAL', code: '3D000', message: `database "${requested}" does not exist` });
|
|
806
|
+
},
|
|
789
807
|
async onMessage(data, { isAuthenticated, clientParams }) {
|
|
790
808
|
if (!isAuthenticated) return undefined; // gateway owns startup/auth traffic
|
|
791
809
|
const client = data;
|