Skip to content

Commit

Permalink
example(hostname): example extension to print system hostname (#100)
Browse files Browse the repository at this point in the history
  • Loading branch information
divyenduz authored Oct 25, 2024
1 parent 57b5bec commit a985f6b
Show file tree
Hide file tree
Showing 11 changed files with 297 additions and 2 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion examples/char_count_zig/build.zig.zon
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
.{
.name = "pg_audit_zig",
.name = "char_count_zig",
.version = "0.1.0",
.paths = .{
"extension",
Expand Down
94 changes: 94 additions & 0 deletions examples/pghostname_zig/README.md
Original file line number Diff line number Diff line change
@@ -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
77 changes: 77 additions & 0 deletions examples/pghostname_zig/build.zig
Original file line number Diff line number Diff line change
@@ -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(&regress.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);
}
}
13 changes: 13 additions & 0 deletions examples/pghostname_zig/build.zig.zon
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
.{
.name = "pghostname_zig",
.version = "0.1.0",
.paths = .{
"extension",
"src",
},
.dependencies = .{
.pgzx = .{
.path = "./../..",
},
},
}
63 changes: 63 additions & 0 deletions examples/pghostname_zig/ci/run.sh
Original file line number Diff line number Diff line change
@@ -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= <<EOF
Usage: $0 [command]
commands (default 'all'):
all - build nand run tests
build - build and install extension
create_extension - create extension
extension_drop - drop extension
regression_tests - run regression tests
unit_tests - run unit tests
help - show this help message
EOF

case $command in
all) all ;;
build) build ;;
create_extension) create_extension ;;
extension_drop) extension_drop ;;
regression_tests) regression_tests ;;
unit_tests) unit_tests ;;
help) echo "$HELP" ;;
*) echo "$HELP" ;;
esac
7 changes: 7 additions & 0 deletions examples/pghostname_zig/expected/pghostname_zig_test.out
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
CREATE EXTENSION pghostname_zig;
SELECT COALESCE(length(pghostname_zig()), 0) > 0;
?column?
----------
t
(1 row)

4 changes: 4 additions & 0 deletions examples/pghostname_zig/extension/pghostname_zig--0.1.sql
Original file line number Diff line number Diff line change
@@ -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;
5 changes: 5 additions & 0 deletions examples/pghostname_zig/extension/pghostname_zig.control
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions examples/pghostname_zig/sql/pghostname_zig_test.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
CREATE EXTENSION pghostname_zig;

SELECT COALESCE(length(pghostname_zig()), 0) > 0;
28 changes: 28 additions & 0 deletions examples/pghostname_zig/src/main.zig
Original file line number Diff line number Diff line change
@@ -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},
);
}

0 comments on commit a985f6b

Please sign in to comment.