diff --git a/.changeset/modern-cycles-work.md b/.changeset/modern-cycles-work.md new file mode 100644 index 000000000..8d3fb3878 --- /dev/null +++ b/.changeset/modern-cycles-work.md @@ -0,0 +1,5 @@ +--- +'@electric-sql/pglite': minor +--- + +Upgraded to PostgreSQL 18.3 diff --git a/docs/extensions/extensions.data.ts b/docs/extensions/extensions.data.ts index 2643be5c4..0c652c7a8 100644 --- a/docs/extensions/extensions.data.ts +++ b/docs/extensions/extensions.data.ts @@ -632,6 +632,20 @@ const baseExtensions: Extension[] = [ importName: 'pg_textsearch', size: 55062, }, + { + name: 'pg_stat_statements', + description: ` + The pg_stat_statements module provides a means for tracking planning and + execution statistics of all SQL statements executed by a server. + `, + shortDescription: 'Track planning and execution statistics of statements', + repo: 'https://www.postgresql.org/docs/current/pgstatstatements.html', + tags: ['postgres extension', 'postgres/contrib'], + importPath: '@electric-sql/pglite/contrib/pg_stat_statements', + importName: 'pg_stat_statements', + core: true, + size: 11554, + }, ] const tags = [ diff --git a/docs/repl/allExtensions.ts b/docs/repl/allExtensions.ts index 9c597c448..c1a77a695 100644 --- a/docs/repl/allExtensions.ts +++ b/docs/repl/allExtensions.ts @@ -1,6 +1,7 @@ export { amcheck } from '@electric-sql/pglite/contrib/amcheck' export { auto_explain } from '@electric-sql/pglite/contrib/auto_explain' export { bloom } from '@electric-sql/pglite/contrib/bloom' +export { pg_stat_statements } from '@electric-sql/pglite/contrib/pg_stat_statements' export { btree_gin } from '@electric-sql/pglite/contrib/btree_gin' export { btree_gist } from '@electric-sql/pglite/contrib/btree_gist' export { citext } from '@electric-sql/pglite/contrib/citext' diff --git a/package.json b/package.json index 4bc55b3fe..c11c86c93 100644 --- a/package.json +++ b/package.json @@ -16,11 +16,12 @@ "wasm:copy-initdb": "mkdir -p ./packages/pglite/release && cp ./postgres-pglite/dist/bin/initdb.* ./packages/pglite/release", "wasm:copy-pgdump": "mkdir -p ./packages/pglite-tools/release && cp ./postgres-pglite/dist/bin/pg_dump.* ./packages/pglite-tools/release", "wasm:copy-pglite": "mkdir -p ./packages/pglite/release/ && cp ./postgres-pglite/dist/bin/pglite.* ./packages/pglite/release/ && cp ./postgres-pglite/dist/extensions/*.tar.gz ./packages/pglite/release/", - "wasm:build": "cd postgres-pglite && ./build-with-docker.sh && cd .. && pnpm wasm:copy-pglite && pnpm wasm:copy-pgdump && pnpm wasm:copy-initdb && pnpm wasm:copy-postgis", + "wasm:build": "cd postgres-pglite && PGLITE_VERSION=$(pnpm -s pglite:version) ./build-with-docker.sh && cd .. && pnpm wasm:copy-pglite && pnpm wasm:copy-pgdump && pnpm wasm:copy-initdb && pnpm wasm:copy-postgis", "wasm:build:debug": "DEBUG=true pnpm wasm:build", "build:all": "pnpm wasm:build && pnpm ts:build", "build:all:debug": "DEBUG=true pnpm build:all", - "prepare": "husky" + "prepare": "husky", + "pglite:version": "pnpm -s -C packages/pglite version" }, "devDependencies": { "@changesets/cli": "^2.27.9", diff --git a/packages/pglite-tools/src/pg_dump.ts b/packages/pglite-tools/src/pg_dump.ts index acd32dd2b..7fb2f1a23 100644 --- a/packages/pglite-tools/src/pg_dump.ts +++ b/packages/pglite-tools/src/pg_dump.ts @@ -170,7 +170,13 @@ export async function pgDump({ ) } - const file = new File([execResult.fileContents], fileName, { + // remove \\restrict and \\unrestrict commands https://www.postgresql.org/docs/17/app-psql.html#APP-PSQL-META-COMMAND-RESTRICT + const contentsPruned = execResult.fileContents.replace( + /^(?:\\(?:un)?restrict\b.*\r?\n?)/gim, + '', + ) + + const file = new File([contentsPruned], fileName, { type: 'text/plain', }) diff --git a/packages/pglite/package.json b/packages/pglite/package.json index 82bec0cf9..2111ade76 100644 --- a/packages/pglite/package.json +++ b/packages/pglite/package.json @@ -210,7 +210,8 @@ "format": "prettier --write ./src ./tests", "typecheck": "tsc --noEmit", "stylecheck": "pnpm lint && prettier --check ./src ./tests", - "prepublishOnly": "pnpm check:exports" + "prepublishOnly": "pnpm check:exports", + "version": "echo $npm_package_version" }, "devDependencies": { "@arethetypeswrong/cli": "^0.18.1", diff --git a/packages/pglite/src/contrib/pg_stat_statements.ts b/packages/pglite/src/contrib/pg_stat_statements.ts new file mode 100644 index 000000000..01ddb6a54 --- /dev/null +++ b/packages/pglite/src/contrib/pg_stat_statements.ts @@ -0,0 +1,20 @@ +import type { + Extension, + ExtensionSetupResult, + PGliteInterface, +} from '../interface' + +const setup = async (_pg: PGliteInterface, _emscriptenOpts: any) => { + return { + bundlePath: new URL( + '../../release/pg_stat_statements.tar.gz', + import.meta.url, + ), + sharedPreloadLibraries: ['pg_stat_statements'], + } satisfies ExtensionSetupResult +} + +export const pg_stat_statements = { + name: 'pg_stat_statements', + setup, +} satisfies Extension diff --git a/packages/pglite/src/interface.ts b/packages/pglite/src/interface.ts index f3c24258f..07c88b343 100644 --- a/packages/pglite/src/interface.ts +++ b/packages/pglite/src/interface.ts @@ -44,6 +44,7 @@ export interface ExtensionSetupResult { emscriptenOpts?: any namespaceObj?: TNamespace bundlePath?: URL + sharedPreloadLibraries?: string[] init?: () => Promise close?: () => Promise } diff --git a/packages/pglite/src/pg_textsearch/index.ts b/packages/pglite/src/pg_textsearch/index.ts index 4e030c4e7..457bf1c7a 100644 --- a/packages/pglite/src/pg_textsearch/index.ts +++ b/packages/pglite/src/pg_textsearch/index.ts @@ -8,6 +8,7 @@ const setup = async (_pg: PGliteInterface, emscriptenOpts: any) => { return { emscriptenOpts, bundlePath: new URL('../../release/pg_textsearch.tar.gz', import.meta.url), + sharedPreloadLibraries: ['pg_textsearch'], } satisfies ExtensionSetupResult } diff --git a/packages/pglite/src/pglite.ts b/packages/pglite/src/pglite.ts index b79413172..9f4679a04 100644 --- a/packages/pglite/src/pglite.ts +++ b/packages/pglite/src/pglite.ts @@ -145,6 +145,10 @@ export class PGlite 'max_parallel_workers=0', '-c', 'max_parallel_workers_per_gather=0', + '-c', + 'io_method=sync', + '-c', + 'max_parallel_maintenance_workers=0', ] /** @@ -475,6 +479,7 @@ export class PGlite // - namespaceObj: The namespace object to attach to the PGlite instance // - init: A function to initialize the extension/plugin after the database is ready // - close: A function to close/tidy-up the extension/plugin when the database is closed + const extSharedPreloadLibraries: string[] = [] for (const [extName, ext] of Object.entries(this.#extensions)) { if (ext instanceof URL) { // Extension with only a URL to a bundle @@ -500,6 +505,7 @@ export class PGlite if (extRet.close) { this.#extensionsClose.push(extRet.close) } + extSharedPreloadLibraries.push(...(extRet.sharedPreloadLibraries ?? [])) } } emscriptenOpts['pg_extensions'] = extensionBundlePromises @@ -570,17 +576,7 @@ export class PGlite // Start compiling dynamic extensions present in FS. await loadExtensions(this.mod, (...args) => this.#log(...args)) - if (options.postgresqlconf) { - const conf = - typeof options.postgresqlconf === 'string' - ? options.postgresqlconf - : options.postgresqlconf.join('\n') - copyToFS( - this.mod.FS, - `${PGDATA}/postgresql.conf`, - new TextEncoder().encode(conf), - ) - } + this.#handlePostgresqlConf(extSharedPreloadLibraries, options) this.mod!._pgl_setPGliteActive(1) this.#startInSingleMode({ @@ -612,6 +608,40 @@ export class PGlite } } + #handlePostgresqlConf( + extSharedPreloadLibraries: string[], + options: PGliteOptions, + ) { + if (extSharedPreloadLibraries.length && !options.postgresqlconf) { + options.postgresqlconf = new Array() + } + + if (options.postgresqlconf) { + let conf = + typeof options.postgresqlconf === 'string' + ? options.postgresqlconf + : options.postgresqlconf.join('\n') + + if (extSharedPreloadLibraries.length) { + const splMatch = conf.match(/^(shared_preload_libraries\s*=\s*)(.*)$/m) + if (splMatch) { + const existing = splMatch[2].split(',').map((s) => s.trim()) + const merged = [ + ...new Set([...existing, ...extSharedPreloadLibraries]), + ] + conf = conf.replace(splMatch[0], `${splMatch[1]}${merged.join(',')}`) + } else { + conf += `\nshared_preload_libraries=${extSharedPreloadLibraries.join(',')}` + } + } + copyToFS( + this.mod!.FS, + `${PGDATA}/postgresql.conf`, + new TextEncoder().encode(conf), + ) + } + } + async #fillIcuDataDir(icuDataDir: Blob | File) { this.#log( `pglite: icuDataDir specified, removing default icu data dir at ${ICU_DATA_PATH}`, diff --git a/packages/pglite/tests/basic.test.ts b/packages/pglite/tests/basic.test.ts index 386f9a500..732160718 100644 --- a/packages/pglite/tests/basic.test.ts +++ b/packages/pglite/tests/basic.test.ts @@ -764,5 +764,11 @@ await testEsmCjsAndDTC(async (importType) => { }, ]) }) + + it('PGlite version', async () => { + const version = await db.query<{ version: string }>(`select version();`) + const re = /\PGlite \d+\.\d+\.\d+\b/ + expect(re.test(version.rows[0].version)).toBeTruthy() + }) }) }) diff --git a/packages/pglite/tests/contrib/amcheck.test.js b/packages/pglite/tests/contrib/amcheck.test.js index 87ec07efc..284dad4c7 100644 --- a/packages/pglite/tests/contrib/amcheck.test.js +++ b/packages/pglite/tests/contrib/amcheck.test.js @@ -33,17 +33,17 @@ it('amcheck', async () => { { bt_index_check: '', relname: 'pg_proc_proname_args_nsp_index', - relpages: 32, + relpages: 33, }, { bt_index_check: '', relname: 'pg_description_o_c_o_index', - relpages: 23, + relpages: 24, }, { bt_index_check: '', relname: 'pg_attribute_relid_attnam_index', - relpages: 15, + relpages: 16, }, { bt_index_check: '', @@ -67,17 +67,17 @@ it('amcheck', async () => { }, { bt_index_check: '', - relname: 'pg_amop_fam_strat_index', + relname: 'pg_operator_oprname_l_r_n_index', relpages: 6, }, { bt_index_check: '', - relname: 'pg_operator_oprname_l_r_n_index', + relname: 'pg_amop_fam_strat_index', relpages: 6, }, { bt_index_check: '', - relname: 'pg_amop_opr_fam_index', + relname: 'pg_class_relname_nsp_index', relpages: 6, }, ]) diff --git a/packages/pglite/tests/contrib/pg_stat_statements.test.ts b/packages/pglite/tests/contrib/pg_stat_statements.test.ts new file mode 100644 index 000000000..25275762a --- /dev/null +++ b/packages/pglite/tests/contrib/pg_stat_statements.test.ts @@ -0,0 +1,56 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest' +import { testEsmCjsAndDTC } from '../test-utils.ts' +import { PGlite } from '../../dist/index.js' + +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')) + + const { pg_stat_statements } = + importType === 'esm' + ? await import('../../dist/contrib/pg_stat_statements.js') + : ((await import( + '../../dist/contrib/pg_stat_statements.cjs' + )) as unknown as typeof import('../../dist/contrib/pg_stat_statements.js')) + + describe(`pg_stat_statements`, () => { + let db: PGlite + let dataDirArchive: File | Blob | undefined = undefined + beforeEach(async () => { + if (!dataDirArchive) { + db = await PGlite.create({ + extensions: { pg_stat_statements }, + }) + dataDirArchive = await db.dumpDataDir('gzip') + } else { + db = await PGlite.create({ + extensions: { pg_stat_statements }, + loadDataDir: dataDirArchive, + }) + } + await db.exec('CREATE EXTENSION IF NOT EXISTS pg_stat_statements;') + }) + + afterEach(async () => { + if (!db.closed) { + await db.close() + } + }) + + it('can load extension', async () => { + // Verify the extension is loaded + const res = await db.query<{ extname: string }>(` + SELECT extname + FROM pg_extension + WHERE extname = 'pg_stat_statements' + `) + + expect(res.rows).toHaveLength(1) + expect(res.rows[0].extname).toBe('pg_stat_statements') + }) + }) +}) diff --git a/postgres-pglite b/postgres-pglite index 06c837c6a..8812f276e 160000 --- a/postgres-pglite +++ b/postgres-pglite @@ -1 +1 @@ -Subproject commit 06c837c6a30335169df786c610ed224bc51d81c0 +Subproject commit 8812f276edbb0381d0bd8e7c305d5a87785c38fe