From 89239677de1a98463193d3ee47887a913da6d40f Mon Sep 17 00:00:00 2001 From: Niklas Mack Date: Sun, 19 Jul 2026 20:14:54 +1200 Subject: [PATCH] Add moddatetime extension support --- docs/repl/allExtensions.ts | 1 + packages/pglite/package.json | 1 - packages/pglite/src/contrib/moddatetime.ts | 16 +++++ .../pglite/tests/contrib/moddatetime.test.ts | 61 +++++++++++++++++++ 4 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 packages/pglite/src/contrib/moddatetime.ts create mode 100644 packages/pglite/tests/contrib/moddatetime.test.ts diff --git a/docs/repl/allExtensions.ts b/docs/repl/allExtensions.ts index cffd8b5f4..30f363551 100644 --- a/docs/repl/allExtensions.ts +++ b/docs/repl/allExtensions.ts @@ -40,3 +40,4 @@ export { unaccent } from '@electric-sql/pglite/contrib/unaccent' export { uuid_ossp } from '@electric-sql/pglite/contrib/uuid_ossp' export { vector } from '@electric-sql/pglite-pgvector' export { pgmq } from '@electric-sql/pglite-pgmq' +export { moddatetime } from '@electric-sql/pglite/contrib/moddatetime' diff --git a/packages/pglite/package.json b/packages/pglite/package.json index dbc6e97ab..088700e2e 100644 --- a/packages/pglite/package.json +++ b/packages/pglite/package.json @@ -110,7 +110,6 @@ "!dist/pglite.html", "!dist/pgrowlocks.tar.gz", "!dist/pgstattuple.tar.gz", - "!dist/spi.tar.gz", "!dist/test_decoding.tar.gz", "!dist/vacuumlo.tar.gz", "!dist/xml2.tar.gz", diff --git a/packages/pglite/src/contrib/moddatetime.ts b/packages/pglite/src/contrib/moddatetime.ts new file mode 100644 index 000000000..5034a50e8 --- /dev/null +++ b/packages/pglite/src/contrib/moddatetime.ts @@ -0,0 +1,16 @@ +import type { + Extension, + ExtensionSetupResult, + PGliteInterface, +} from '../interface' + +const setup = async (_pg: PGliteInterface, _emscriptenOpts: any) => { + return { + bundlePath: new URL('../../release/spi.tar.gz', import.meta.url), + } satisfies ExtensionSetupResult +} + +export const moddatetime = { + name: 'moddatetime', + setup, +} satisfies Extension diff --git a/packages/pglite/tests/contrib/moddatetime.test.ts b/packages/pglite/tests/contrib/moddatetime.test.ts new file mode 100644 index 000000000..72dddfd4a --- /dev/null +++ b/packages/pglite/tests/contrib/moddatetime.test.ts @@ -0,0 +1,61 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest' +import { PGlite } from '../../dist/index.js' +import { moddatetime } from '../../dist/contrib/moddatetime.js' + +describe('moddatetime', () => { + let pg: PGlite + let dataDirArchive: File | Blob + beforeEach(async () => { + if (!dataDirArchive) { + pg = await PGlite.create({ + extensions: { moddatetime }, + }) + dataDirArchive = await pg.dumpDataDir('gzip') + } else { + pg = await PGlite.create({ + extensions: { moddatetime }, + loadDataDir: dataDirArchive, + }) + } + await pg.exec('CREATE EXTENSION IF NOT EXISTS "moddatetime";') + }) + afterEach(async () => { + if (!pg.closed) { + await pg.close() + } + }) + + it('moddatetime', async () => { + await pg.exec(` + CREATE OR REPLACE FUNCTION manage_updated_at(_tbl regclass) RETURNS VOID AS $$ + BEGIN + EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s + FOR EACH ROW EXECUTE PROCEDURE moddatetime("updatedAt")', _tbl); + END; + $$ LANGUAGE plpgsql; + `) + + await pg.exec(` + CREATE TABLE "users" ( + "id" SERIAL PRIMARY KEY, + "name" TEXT NOT NULL, + "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), + "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW() + ); + + SELECT manage_updated_at('users'); + `) + + await pg.exec(`INSERT INTO "users" ("name") VALUES ('test');`) + const res = await pg.query(`SELECT * FROM "users"`) + expect(res.rows[0].updatedAt.getTime()).toBe( + res.rows[0].createdAt.getTime(), + ) + + await pg.exec(`UPDATE "users" SET "name" = 'test2' WHERE "name" = 'test';`) + const res2 = await pg.query(`SELECT * FROM "users"`) + expect(res2.rows[0].updatedAt.getTime()).toBeGreaterThan( + res2.rows[0].createdAt.getTime(), + ) + }) +})