Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/pglite/src/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,12 @@ export interface PGliteOptions<TExtensions extends Extensions = Extensions> {
dataDir?: string
username?: string
database?: string
/**
* Extra postgres GUC settings passed as `-c key=value` at backend startup.
* Use this for postmaster settings that must be set before the server starts
* (for example `wal_level=logical`).
*/
postgresConfig?: Record<string, string | number | boolean>
fs?: Filesystem
debug?: DebugLevel
relaxedDurability?: boolean
Expand Down
73 changes: 73 additions & 0 deletions packages/pglite/src/pglite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ import {
NotificationResponseMessage,
} from '@electric-sql/pg-protocol/messages'

const POSTGRES_CONFIG_ENV_NAME = 'PGLITE_POSTGRES_CONFIG'

export class PGlite
extends BasePGlite
implements PGliteInterface, AsyncDisposable
Expand Down Expand Up @@ -212,12 +214,18 @@ export class PGlite

const extensionBundlePromises: Record<string, Promise<Blob | null>> = {}
const extensionInitFns: Array<() => Promise<void>> = []
const serializedPostgresConfig = serializePostgresConfig(
options.postgresConfig,
)

const args = [
`PGDATA=${PGDATA}`,
`PREFIX=${WASM_PREFIX}`,
`PGUSER=${options.username ?? 'postgres'}`,
`PGDATABASE=${options.database ?? 'template1'}`,
...(serializedPostgresConfig
? [`${POSTGRES_CONFIG_ENV_NAME}=${serializedPostgresConfig}`]
: []),
'MODE=REACT',
'REPL=N',
// "-F", // Disable fsync (TODO: Only for in-memory mode?)
Expand Down Expand Up @@ -584,6 +592,9 @@ export class PGlite
// Database closed successfully
// An earlier build of PGlite would throw an error here when closing
// leaving this here for now. I believe it was a bug in Emscripten.
} else if (e === Infinity) {
// Some emscripten shutdown paths can surface a numeric sentinel
// instead of ExitStatus(0) even though shutdown completed.
} else {
throw e
}
Expand Down Expand Up @@ -1002,3 +1013,65 @@ export class PGlite
return this.#listenMutex.runExclusive(fn)
}
}

const POSTGRES_GUC_NAME_RE = /^[A-Za-z_][A-Za-z0-9_.-]*$/
const POSTGRES_CONFIG_ENTRY_SEPARATOR = ';'

function serializePostgresConfig(
config?: Record<string, string | number | boolean>,
) {
if (!config) {
return null
}

const entries = Object.entries(config).sort(([a], [b]) => a.localeCompare(b))
if (entries.length === 0) {
return null
}

return entries
.map(
([name, value]) =>
`${formatPostgresGucName(name)}=${formatPostgresGucValue(value)}`,
)
.join(POSTGRES_CONFIG_ENTRY_SEPARATOR)
}

function formatPostgresGucName(name: string) {
if (!POSTGRES_GUC_NAME_RE.test(name)) {
throw new Error(`Invalid postgresConfig key: ${name}`)
}
if (name.includes(POSTGRES_CONFIG_ENTRY_SEPARATOR)) {
throw new Error(
`Invalid postgresConfig key (contains '${POSTGRES_CONFIG_ENTRY_SEPARATOR}'): ${name}`,
)
}
return name
}

function formatPostgresGucValue(value: string | number | boolean) {
let normalized: string
if (typeof value === 'number') {
if (!Number.isFinite(value)) {
throw new Error(`Invalid postgresConfig value: ${value}`)
}
normalized = String(value)
} else if (typeof value === 'boolean') {
normalized = value ? 'on' : 'off'
} else {
normalized = value
}

if (
normalized.includes(POSTGRES_CONFIG_ENTRY_SEPARATOR) ||
normalized.includes('\n') ||
normalized.includes('\r') ||
normalized.includes('\0')
) {
throw new Error(
`Invalid postgresConfig value (contains disallowed separator/control character): ${normalized}`,
)
}

return normalized
}
48 changes: 48 additions & 0 deletions packages/pglite/tests/postgres-config.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { describe, expect, it } from 'vitest'
import { testEsmCjsAndDTC } from './test-utils.ts'

await testEsmCjsAndDTC(async (importType) => {
const { PGlite } =
importType === 'esm'
? await import('../dist/index.js')
: ((await import(
'../dist/index.cjs'
)) as unknown as typeof import('../dist/index.js'))

describe('postgresConfig', () => {
it('applies postmaster settings at startup', async () => {
const db = await PGlite.create({
dataDir: 'memory://',
postgresConfig: {
max_replication_slots: 12,
max_wal_senders: 12,
wal_level: 'logical',
},
})

const settings = await db.query<{
name: string
setting: string
}>(
"SELECT name, setting FROM pg_settings WHERE name IN ('max_replication_slots', 'max_wal_senders', 'wal_level') ORDER BY name",
)

expect(settings.rows).toEqual([
{
name: 'max_replication_slots',
setting: '12',
},
{
name: 'max_wal_senders',
setting: '12',
},
{
name: 'wal_level',
setting: 'logical',
},
])

await db.close()
})
})
})
2 changes: 1 addition & 1 deletion postgres-pglite