Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

example(hostname): example extension to print system hostname #100

Merged
merged 13 commits into from
Oct 25, 2024
Merged
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
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},
);
}