From a985f6b7825af2b054d8cbe1d7141545227fe63b Mon Sep 17 00:00:00 2001 From: Divyendu Singh Date: Fri, 25 Oct 2024 12:26:01 +0200 Subject: [PATCH] example(hostname): example extension to print system hostname (#100) Port of https://github.com/theory/pg-hostname/ --- README.md | 3 +- examples/char_count_zig/build.zig.zon | 2 +- examples/pghostname_zig/README.md | 94 +++++++++++++++++++ examples/pghostname_zig/build.zig | 77 +++++++++++++++ examples/pghostname_zig/build.zig.zon | 13 +++ examples/pghostname_zig/ci/run.sh | 63 +++++++++++++ .../expected/pghostname_zig_test.out | 7 ++ .../extension/pghostname_zig--0.1.sql | 4 + .../extension/pghostname_zig.control | 5 + .../sql/pghostname_zig_test.sql | 3 + examples/pghostname_zig/src/main.zig | 28 ++++++ 11 files changed, 297 insertions(+), 2 deletions(-) create mode 100644 examples/pghostname_zig/README.md create mode 100644 examples/pghostname_zig/build.zig create mode 100644 examples/pghostname_zig/build.zig.zon create mode 100755 examples/pghostname_zig/ci/run.sh create mode 100644 examples/pghostname_zig/expected/pghostname_zig_test.out create mode 100644 examples/pghostname_zig/extension/pghostname_zig--0.1.sql create mode 100644 examples/pghostname_zig/extension/pghostname_zig.control create mode 100644 examples/pghostname_zig/sql/pghostname_zig_test.sql create mode 100644 examples/pghostname_zig/src/main.zig diff --git a/README.md b/README.md index 02aaf77..ec3920c 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,8 @@ The following sample extensions (ordered from simple to complex) show how to use | Extension | Description | |--------------------------------------------|-------------| | [char_count_zig](examples/char_count_zig/) | Adds a function that counts how many times a particular character shows up in a string. Shows how to register a function and how to interpret the parameters. | -| [pg_audit_zig](examples/pgaudit_zig/) | Inspired by the pgaudit C extension, this one registers callbacks to multiple hooks and uses more advanced error handling and memory allocation patterns | +| [pghostname_zig](examples/pghostname_zig/) | Adds a function that returns the database server's host name. | +| [pg_audit_zig](examples/pgaudit_zig/) | Inspired by the pgaudit C extension, this one registers callbacks to multiple hooks and uses more advanced error handling and memory allocation patterns. | ## Docs diff --git a/examples/char_count_zig/build.zig.zon b/examples/char_count_zig/build.zig.zon index 69ca583..a4c12df 100644 --- a/examples/char_count_zig/build.zig.zon +++ b/examples/char_count_zig/build.zig.zon @@ -1,5 +1,5 @@ .{ - .name = "pg_audit_zig", + .name = "char_count_zig", .version = "0.1.0", .paths = .{ "extension", diff --git a/examples/pghostname_zig/README.md b/examples/pghostname_zig/README.md new file mode 100644 index 0000000..4d54dfa --- /dev/null +++ b/examples/pghostname_zig/README.md @@ -0,0 +1,94 @@ +# pghostname_zig - Minimal PostgreSQL extension using Zig + +This is a sample PostgreSQL extension written in Zig. It provides a function `pghostname_zig` that returns the database server's host name. The code is a port using pgzx of the `pg-hostname` C extension from [this repo](https://github.com/theory/pg-hostname/). + +## Functionality + +The function `pghostname_zig` takes no arguments. It returns the database server's host name: + +```sql +SELECT pghostname_zig(); + pghostname_zig +---------------- + ubuntu +(1 row) +``` + +## Running + +To test the extension, follow first the development shell instructions in the [pgzx README][pgzx_Development]. The following commands assume you are in the nix shell (run `nix develop`). + +Run in the folder of the extension: + +```sh +cd examples/pghostname_zig +zig build -freference-trace -p $PG_HOME +``` + +This will build the extension and install the extension in the Postgres instance. + +Then, connect to the Postgres instance: + +```sh +psql -U postgres +``` + +At the Postgres prompt, create the extension: + +```sql +CREATE EXTENSION pghostname_zig; +``` + +## Code walkthrough + +### Control files + +The overall structure of the extension looks very similar to a C extension: + +``` +├── extension +│ ├── pghostname_zig--0.1.sql +│ └── pghostname_zig.control +``` + +The `extension` folder contains the control files, which are used by Postgres to manage the extension. The `pghostname_zig.control` file contains metadata about the extension, such as its name and version. The `pghostname_zig--0.1.sql` file contains the SQL commands to create and drop the extension. + +### Zig code + +The `main.zig` file starts with the following `comptime` block: + +```zig +comptime { + pgzx.PG_MODULE_MAGIC(); + + pgzx.PG_FUNCTION_V1("pghostname_zig", pghostname_zig); +} +``` + +The [pgzx.PG_MODULE_MAGIC][docs_PG_MODULE_MAGIC] function returns an exported `PG_MAGIC` struct that PostgreSQL uses to recognize the library as a Postgres extension. + +The [pgzx.PG_FUNCTION_V1][docs_PG_FUNCTION_V1] macro defines the `pghostname_zig` function as a Postgres function. This function does the heavy lifting of deserializing the input arguments and transforming them in Zig slices. + +This means the implementation of the `pghostname_zig` function is quite simple: + +```zig +pub fn pghostname_zig() ![]const u8 { + var buffer: [std.posix.HOST_NAME_MAX]u8 = undefined; + const hostname = std.posix.gethostname(&buffer) catch "unknown"; + return try pgzx.mem.PGCurrentContextAllocator.dupeZ(u8, hostname); +} +``` + +### Testing + +The extensions contains regression tests using the `pg_regress` tool, see the `sql` and `expected` folders. To run the regression tests, use the following command: + +```sh +zig build pg_regress +``` + +[pgzx_Development]: https://github.com/xataio/pgzx/tree/main?tab=readme-ov-file#develpment-shell-and-local-installation +[docs_PG_MODULE_MAGIC]: https://xataio.github.io/pgzx/#A;pgzx:fmgr.PG_MAGIC +[docs_PG_FUNCTION_V1]: https://xataio.github.io/pgzx/#A;pgzx:PG_FUNCTION_V1 +[docs_Error]: https://xataio.github.io/pgzx/#A;pgzx:elog.Error +[docs_Info]: https://xataio.github.io/pgzx/#A;pgzx:elog.Info diff --git a/examples/pghostname_zig/build.zig b/examples/pghostname_zig/build.zig new file mode 100644 index 0000000..87fbada --- /dev/null +++ b/examples/pghostname_zig/build.zig @@ -0,0 +1,77 @@ +const std = @import("std"); + +// Load pgzx build support. The build utilities use pg_config to find all dependencies +// and provide functions go create and test extensions. +const PGBuild = @import("pgzx").Build; + +pub fn build(b: *std.Build) void { + const NAME = "pghostname_zig"; + const VERSION = .{ .major = 0, .minor = 1 }; + + const DB_TEST_USER = "postgres"; + const DB_TEST_PORT = 5432; + + const build_options = b.addOptions(); + build_options.addOption(bool, "testfn", b.option(bool, "testfn", "Register test function") orelse false); + + var proj = PGBuild.Project.init(b, .{ + .name = NAME, + .version = VERSION, + .root_dir = "src/", + .root_source_file = "src/main.zig", + }); + proj.addOptions("build_options", build_options); + + const steps = .{ + .check = b.step("check", "Check if project compiles"), + .install = b.getInstallStep(), + .pg_regress = b.step("pg_regress", "Run regression tests"), + .unit = b.step("unit", "Run unit tests"), + }; + + { // build and install extension + steps.install.dependOn(&proj.installExtensionLib().step); + steps.install.dependOn(&proj.installExtensionDir().step); + } + + { // check extension Zig source code only. No linkage or installation for faster development. + const lib = proj.extensionLib(); + lib.linkage = null; + steps.check.dependOn(&lib.step); + } + + { // pg_regress tests (regression tests use the default build) + var regress = proj.pgbuild.addRegress(.{ + .db_user = DB_TEST_USER, + .db_port = DB_TEST_PORT, + .root_dir = ".", + .scripts = &[_][]const u8{ + "pghostname_zig_test", + }, + }); + regress.step.dependOn(steps.install); + + steps.pg_regress.dependOn(®ress.step); + } + + { // unit testing. We install an alternative version of the lib build with test_fn = true + const test_options = b.addOptions(); + test_options.addOption(bool, "testfn", true); + + const lib = proj.extensionLib(); + lib.root_module.addOptions("build_options", test_options); + + // Step for running the unit tests. + const psql_run_tests = proj.pgbuild.addRunTests(.{ + .name = NAME, + .db_user = DB_TEST_USER, + .db_port = DB_TEST_PORT, + }); + + // Build and install extension before running the tests. + psql_run_tests.step.dependOn(&proj.pgbuild.addInstallExtensionLibArtifact(lib, NAME).step); + psql_run_tests.step.dependOn(&proj.installExtensionLib().step); + + steps.unit.dependOn(&psql_run_tests.step); + } +} diff --git a/examples/pghostname_zig/build.zig.zon b/examples/pghostname_zig/build.zig.zon new file mode 100644 index 0000000..9bdf543 --- /dev/null +++ b/examples/pghostname_zig/build.zig.zon @@ -0,0 +1,13 @@ +.{ + .name = "pghostname_zig", + .version = "0.1.0", + .paths = .{ + "extension", + "src", + }, + .dependencies = .{ + .pgzx = .{ + .path = "./../..", + }, + }, +} diff --git a/examples/pghostname_zig/ci/run.sh b/examples/pghostname_zig/ci/run.sh new file mode 100755 index 0000000..5d8bcdb --- /dev/null +++ b/examples/pghostname_zig/ci/run.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash + +#set -x +set -o pipefail + +EXTENSION_NAME=pghostname_zig + +build() { + echo "Build extension $EXTENSION_NAME" + zig build -freference-trace -p "$PG_HOME" || return 1 +} + +create_extension() { + echo "Create extension $EXTENSION_NAME" + psql -U postgres -c "CREATE EXTENSION IF NOT EXISTS $EXTENSION_NAME" +} + +extension_drop() { + echo "Drop extension $EXTENSION_NAME" + psql -U postgres -c "DROP EXTENSION IF EXISTS $EXTENSION_NAME" +} + +regression_tests() { + echo "Run regression tests: $EXTENSION_NAME" + zig build pg_regress --verbose || return 1 +} + +unit_tests() { + echo "Run unit tests: $EXTENSION_NAME" + zig build -freference-trace -p "$PG_HOME" unit || return 1 +} + +all() { + build && create_extension && unit_tests && regression_tests && extension_drop +} + +# optional command. Use all if not specified +command=${1:-all} + +#shellcheck disable=SC1007 +HELP= < 0; + ?column? +---------- + t +(1 row) + diff --git a/examples/pghostname_zig/extension/pghostname_zig--0.1.sql b/examples/pghostname_zig/extension/pghostname_zig--0.1.sql new file mode 100644 index 0000000..f10c066 --- /dev/null +++ b/examples/pghostname_zig/extension/pghostname_zig--0.1.sql @@ -0,0 +1,4 @@ +\echo Use "CREATE EXTENSION pghostname_zig" to load this file. \quit +CREATE FUNCTION pghostname_zig() RETURNS TEXT +AS '$libdir/pghostname_zig' +LANGUAGE C IMMUTABLE; \ No newline at end of file diff --git a/examples/pghostname_zig/extension/pghostname_zig.control b/examples/pghostname_zig/extension/pghostname_zig.control new file mode 100644 index 0000000..6779808 --- /dev/null +++ b/examples/pghostname_zig/extension/pghostname_zig.control @@ -0,0 +1,5 @@ +# pghostname_zig extension +comment = 'zig function to get the system hostname' +default_version = '0.1' +module_pathname = '$libdir/pghostname_zig' +relocatable = true diff --git a/examples/pghostname_zig/sql/pghostname_zig_test.sql b/examples/pghostname_zig/sql/pghostname_zig_test.sql new file mode 100644 index 0000000..881b939 --- /dev/null +++ b/examples/pghostname_zig/sql/pghostname_zig_test.sql @@ -0,0 +1,3 @@ +CREATE EXTENSION pghostname_zig; + +SELECT COALESCE(length(pghostname_zig()), 0) > 0; diff --git a/examples/pghostname_zig/src/main.zig b/examples/pghostname_zig/src/main.zig new file mode 100644 index 0000000..4b66c0d --- /dev/null +++ b/examples/pghostname_zig/src/main.zig @@ -0,0 +1,28 @@ +const std = @import("std"); +const pgzx = @import("pgzx"); + +comptime { + pgzx.PG_MODULE_MAGIC(); + + pgzx.PG_EXPORT(@This()); +} + +pub fn pghostname_zig() ![:0]const u8 { + var buffer: [std.posix.HOST_NAME_MAX]u8 = undefined; + const hostname = std.posix.gethostname(&buffer) catch "unknown"; + return try pgzx.mem.PGCurrentContextAllocator.dupeZ(u8, hostname); +} + +const Testsuite1 = struct { + pub fn testHappyPath() !void { + const hostname = try pghostname_zig(); + try std.testing.expect(hostname.len > 0); + } +}; + +comptime { + pgzx.testing.registerTests( + @import("build_options").testfn, + .{Testsuite1}, + ); +}