├── .gitignore ├── .github └── workflows │ └── ci.yml ├── LICENSE ├── README.md └── src ├── messaging.zig ├── encdec.zig ├── auth.zig └── pgz.zig /.gitignore: -------------------------------------------------------------------------------- 1 | .zig-cache/ 2 | zig-out/ 3 | docs/ -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push] 3 | jobs: 4 | test-postgres: 5 | runs-on: ubuntu-latest 6 | services: 7 | postgres: 8 | image: postgres 9 | ports: 10 | - 5432:5432 11 | env: 12 | POSTGRES_USER: testing 13 | POSTGRES_PASSWORD: testing 14 | POSTGRES_DB: testing 15 | options: >- 16 | --health-cmd pg_isready 17 | --health-interval 10s 18 | --health-timeout 5s 19 | --health-retries 5 20 | steps: 21 | - uses: actions/checkout@v3 22 | - name: Install dependencies 23 | run: | 24 | sudo snap install zig --classic --edge 25 | zig version 26 | - name: Run tests 27 | run: zig build test 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Behzod Mansurov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | ![](https://img.shields.io/github/actions/workflow/status/star-tek-mb/pgz/ci.yml) 4 | ![](https://img.shields.io/badge/version-0.0.1-red) 5 | ![](https://img.shields.io/github/license/star-tek-mb/pgz) 6 | ![https://pgz.decoy.uz](https://img.shields.io/badge/docs-passing-green) 7 | 8 | **pgz** - postgres driver/connector written in Zig (status pre-alpha development) 9 | 10 | # Package manager ready 11 | 12 | Add following lines to your `build.zig.zon` dependencies: 13 | ```zig 14 | .pgz = .{ 15 | .url = "git+https://github.com/star-tek-mb/pgz#master", 16 | } 17 | ``` 18 | 19 | Run `zig build` then obtain hash of the package and insert it to `build.zig.zon`. 20 | 21 | Then you can use it as a library. Add following lines to `build.zig`: 22 | 23 | ```zig 24 | const pgz_dep = b.dependency("pgz", .{ .target = target, .optimize = optimize }); 25 | 26 | exe.addModule("pgz", pgz_dep.module("pgz")); 27 | ``` 28 | 29 | # Example 30 | 31 | ```zig 32 | const std = @import("std"); 33 | const Connection = @import("pgz").Connection; 34 | 35 | pub fn main() !void { 36 | var dsn = try std.Uri.parse("postgres://testing:testing@localhost:5432/testing"); 37 | var connection = try Connection.init(std.heap.page_allocator, dsn); 38 | defer connection.deinit(); 39 | var result = try connection.query("SELECT 1 as number;", struct { number: ?[]const u8 }); 40 | defer result.deinit(); 41 | 42 | try connection.exec("CREATE TABLE users(name text not null);"); 43 | defer connection.exec("DROP TABLE users;") catch {}; 44 | var stmt = try connection.prepare("INSERT INTO users(name) VALUES($1);"); 45 | defer stmt.deinit(); 46 | try stmt.exec(.{"hello"}); 47 | try stmt.exec(.{"world"}); 48 | 49 | try std.io.getStdOut().writer().print("number = {s}\n", .{result.data[0].number.?}); 50 | } 51 | ``` 52 | 53 | # TODOs 54 | 55 | - Optimize allocations (use stack fallback allocator for messages) 56 | - Fix all todos in code 57 | - Connection pools (do we need them?) 58 | - Complete and test in production? 59 | 60 | # Testing 61 | 62 | Create user `testing` with password `testing`. 63 | 64 | Create database `testing`. 65 | 66 | Run `zig build test` 67 | -------------------------------------------------------------------------------- /src/messaging.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | pub const ReadBuffer = struct { 4 | buf: []const u8, 5 | pos: u32 = 0, 6 | 7 | pub fn init(buf: []const u8) ReadBuffer { 8 | return ReadBuffer{ .buf = buf }; 9 | } 10 | 11 | pub fn readInt(self: *ReadBuffer, comptime T: type) T { 12 | const ret = std.mem.readInt(T, self.buf[self.pos..][0..@sizeOf(T)], .big); 13 | self.pos += @sizeOf(T); 14 | return ret; 15 | } 16 | 17 | pub fn readString(self: *ReadBuffer) []const u8 { 18 | const start = self.pos; 19 | while (self.buf[self.pos] != 0 and self.pos < self.buf.len) : (self.pos += 1) {} 20 | self.pos += 1; 21 | return self.buf[start .. self.pos - 1]; 22 | } 23 | 24 | pub fn readBytes(self: *ReadBuffer, num: u32) []const u8 { 25 | const ret = self.buf[self.pos .. self.pos + num]; 26 | self.pos += num; 27 | return ret; 28 | } 29 | 30 | pub fn reset(self: *ReadBuffer) void { 31 | self.pos = 0; 32 | } 33 | }; 34 | 35 | pub const WriteBuffer = struct { 36 | tag: ?u8 = null, 37 | buf: std.ArrayList(u8), 38 | index: u32 = 0, 39 | 40 | pub fn init(allocator: std.mem.Allocator, maybe_tag: ?u8) !WriteBuffer { 41 | var buf = try std.ArrayList(u8).initCapacity(allocator, 512); 42 | if (maybe_tag) |tag| { 43 | try buf.append(tag); 44 | } 45 | _ = try buf.addManyAsArray(4); 46 | return WriteBuffer{ .buf = buf, .tag = maybe_tag }; 47 | } 48 | 49 | pub fn deinit(self: *WriteBuffer) void { 50 | self.buf.clearAndFree(); 51 | } 52 | 53 | pub fn writeInt(self: *WriteBuffer, comptime T: type, value: T) void { 54 | self.buf.writer().writeInt(T, value, .big) catch {}; 55 | } 56 | 57 | pub fn writeString(self: *WriteBuffer, string: []const u8) void { 58 | self.buf.writer().writeAll(string) catch {}; 59 | self.buf.writer().writeByte(0) catch {}; 60 | } 61 | 62 | pub fn writeBytes(self: *WriteBuffer, bytes: []const u8) void { 63 | self.buf.writer().writeAll(bytes) catch {}; 64 | } 65 | 66 | pub fn finalize(self: *WriteBuffer) void { 67 | if (self.tag == null) { 68 | std.mem.writeInt(u32, self.buf.items[self.index..][0..4], @as(u32, @intCast(self.buf.items.len - self.index)), .big); 69 | } else { 70 | std.mem.writeInt(u32, self.buf.items[self.index + 1 ..][0..4], @as(u32, @intCast(self.buf.items.len - self.index - 1)), .big); 71 | } 72 | } 73 | 74 | pub fn reset(self: *WriteBuffer, maybe_tag: ?u8) void { 75 | self.buf.clearRetainingCapacity(); 76 | self.tag = maybe_tag; 77 | if (maybe_tag) |tag| { 78 | self.buf.append(tag) catch {}; 79 | } 80 | _ = self.buf.addManyAsArray(4) catch {}; 81 | self.index = 0; 82 | } 83 | 84 | pub fn next(self: *WriteBuffer, maybe_tag: ?u8) void { 85 | self.finalize(); 86 | self.index = @as(u32, @intCast(self.buf.items.len)); 87 | self.tag = maybe_tag; 88 | if (maybe_tag) |tag| { 89 | self.buf.append(tag) catch {}; 90 | } 91 | _ = self.buf.addManyAsArray(4) catch {}; 92 | } 93 | 94 | /// finalizes buffer before sending 95 | pub fn send(self: *WriteBuffer, stream: std.net.Stream) !void { 96 | self.finalize(); 97 | try stream.writeAll(self.buf.items); 98 | } 99 | }; 100 | 101 | pub const Message = struct { 102 | type: u8, 103 | len: u32, 104 | msg: []const u8, 105 | 106 | pub fn read(allocator: std.mem.Allocator, reader: anytype) !Message { 107 | var type_and_len: [5]u8 = undefined; 108 | _ = try reader.read(&type_and_len); 109 | const @"type" = type_and_len[0]; 110 | const len = std.mem.readInt(u32, type_and_len[1..][0..4], .big); 111 | if (len > 4) { 112 | const msg = try allocator.alloc(u8, len - 4); 113 | _ = try reader.read(msg); 114 | return Message{ .type = @"type", .len = len, .msg = msg }; 115 | } 116 | return Message{ .type = @"type", .len = len, .msg = &.{} }; 117 | } 118 | 119 | pub fn free(self: *Message, allocator: std.mem.Allocator) void { 120 | allocator.free(self.msg); 121 | } 122 | }; 123 | 124 | test "read buffer" { 125 | const data = [_]u8{ 0, 0, 0, 5, 0, 0, 0, 12, 72, 101, 108, 108, 111, 0, 119, 111, 114, 108, 100, 0 }; 126 | var buf = ReadBuffer.init(&data); 127 | try std.testing.expectEqual(@as(u32, 5), buf.readInt(u32)); 128 | try std.testing.expectEqual(@as(u32, 12), buf.readInt(u32)); 129 | try std.testing.expectEqualStrings("Hello", buf.readString()); 130 | try std.testing.expectEqualStrings("world", buf.readString()); 131 | } 132 | 133 | test "write buffer" { 134 | const data = [_]u8{ 0, 0, 0, 24, 0, 0, 0, 5, 0, 0, 0, 12, 72, 101, 108, 108, 111, 0, 119, 111, 114, 108, 100, 0 }; 135 | var buf = try WriteBuffer.init(std.testing.allocator, null); 136 | defer buf.deinit(); 137 | buf.writeInt(u32, 5); 138 | buf.writeInt(u32, 12); 139 | buf.writeString("Hello"); 140 | buf.writeString("world"); 141 | buf.finalize(); 142 | try std.testing.expectEqualSlices(u8, data[0..], buf.buf.items[0..]); 143 | } 144 | -------------------------------------------------------------------------------- /src/encdec.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const hex_charset = "0123456789abcdef"; 4 | 5 | /// caller owns memory 6 | fn hexEncode(allocator: std.mem.Allocator, string: []const u8) []const u8 { 7 | var ret = allocator.alloc(u8, 2 + string.len * 2); 8 | ret[0] = '\\'; 9 | ret[1] = 'x'; 10 | for (string, 0..) |byte, i| { 11 | ret[(i + 1) * 2 + 0] = hex_charset[byte >> 4]; 12 | ret[(i + 1) * 2 + 1] = hex_charset[byte & 15]; 13 | } 14 | return ret; 15 | } 16 | 17 | /// caller owns memory 18 | pub fn encode(allocator: std.mem.Allocator, value: anytype) ![]const u8 { 19 | switch (@typeInfo(@TypeOf(value))) { 20 | .Bool => { 21 | return try std.fmt.allocPrint(allocator, "{}", .{value}); 22 | }, 23 | .Float, .ComptimeFloat, .Int, .ComptimeInt, .Enum => { 24 | return try std.fmt.allocPrint(allocator, "{}", .{value}); 25 | }, 26 | .Optional => { 27 | return encode(allocator, value.?); // value is definitely not null 28 | }, 29 | .Array => |array| { 30 | if (array.child == u8) { 31 | return try allocator.dupe(u8, value); 32 | } 33 | }, 34 | .Pointer => |pointer| { 35 | switch (pointer.size) { 36 | .One => { 37 | if (@typeInfo(pointer.child) == .Array and @typeInfo(pointer.child).Array.child == u8) { 38 | return try allocator.dupe(u8, value); 39 | } 40 | }, 41 | .Slice => { 42 | if (pointer.child == u8) { 43 | return try allocator.dupe(u8, value); 44 | } 45 | }, 46 | else => {}, 47 | } 48 | }, 49 | else => {}, 50 | } 51 | return error.EncodeError; 52 | } 53 | 54 | /// caller owns memory 55 | pub fn decode(allocator: std.mem.Allocator, string: ?[]const u8, comptime T: type) !T { 56 | if (string == null and @typeInfo(T) == .Optional) return null; 57 | if (string == null and @typeInfo(T) != .Optional) return error.DecodeError; 58 | 59 | switch (@typeInfo(T)) { 60 | .Bool => { 61 | return string.?[0] == 't'; 62 | }, 63 | .Float, .ComptimeFloat => { 64 | return try std.fmt.parseFloat(T, string.?); 65 | }, 66 | .Int, .ComptimeInt => { 67 | return try std.fmt.parseInt(T, string.?, 10); 68 | }, 69 | .Optional => |optional| { 70 | if (string == null) { 71 | return null; 72 | } else { 73 | return try decode(allocator, string, optional.child); 74 | } 75 | }, 76 | .Array => |array| { 77 | if (array.child == u8) { 78 | return try allocator.dupe(u8, string.?); 79 | } 80 | }, 81 | .Pointer => |pointer| { 82 | switch (pointer.size) { 83 | .One => { 84 | if (@typeInfo(pointer.child) == .Array and @typeInfo(pointer.child).Array.child == u8) { 85 | return try allocator.dupe(u8, string.?); 86 | } 87 | }, 88 | .Slice => { 89 | if (pointer.child == u8) { 90 | return try allocator.dupe(u8, string.?); 91 | } 92 | }, 93 | else => {}, 94 | } 95 | }, 96 | else => {}, 97 | } 98 | return error.DecodeError; 99 | } 100 | 101 | /// Quotes identifier, caller owns memory 102 | pub fn quoteIdentifier(allocator: std.mem.Allocator, identifier: []const u8) ![]const u8 { 103 | var buf = try std.ArrayList(u8).initCapacity(allocator, identifier.len + 2); 104 | defer buf.deinit(); 105 | try buf.append('\"'); 106 | for (0..identifier.len) |i| { 107 | if (identifier[i] == '\"') { 108 | try buf.append(identifier[i]); 109 | } 110 | try buf.append(identifier[i]); 111 | } 112 | try buf.append('\"'); 113 | return try buf.toOwnedSlice(); 114 | } 115 | 116 | /// Quotes literal, caller owns memory 117 | pub fn quoteLiteral(allocator: std.mem.Allocator, literal: []const u8) ![]const u8 { 118 | var buf = try std.ArrayList(u8).initCapacity(allocator, literal.len + 2); 119 | defer buf.deinit(); 120 | var has_backslash = false; 121 | var i: usize = 0; 122 | while (i < literal.len) : (i += 1) { 123 | if (literal[i] == '\\') { 124 | try buf.append(literal[i]); 125 | has_backslash = true; 126 | } 127 | if (literal[i] == '\'') { 128 | try buf.append(literal[i]); 129 | } 130 | try buf.append(literal[i]); 131 | } 132 | if (has_backslash) { 133 | try buf.insertSlice(0, " E'"); 134 | try buf.append('\''); 135 | } else { 136 | try buf.insert(0, '\''); 137 | try buf.append('\''); 138 | } 139 | return try buf.toOwnedSlice(); 140 | } 141 | 142 | test "quote identifier" { 143 | const id = try quoteIdentifier(std.testing.allocator, "my_table"); 144 | defer std.testing.allocator.free(id); 145 | try std.testing.expectEqualStrings("\"my_table\"", id); 146 | } 147 | 148 | test "quote literal" { 149 | const q1 = try quoteLiteral(std.testing.allocator, "hello '' world"); 150 | defer std.testing.allocator.free(q1); 151 | try std.testing.expectEqualStrings("'hello '''' world'", q1); 152 | 153 | const q2 = try quoteLiteral(std.testing.allocator, "hello \\'\\' world"); 154 | defer std.testing.allocator.free(q2); 155 | try std.testing.expectEqualStrings(" E'hello \\\\''\\\\'' world'", q2); 156 | } 157 | -------------------------------------------------------------------------------- /src/auth.zig: -------------------------------------------------------------------------------- 1 | // TODO: do not hardcode array sizes 2 | // TODO: tidy this code 3 | 4 | const std = @import("std"); 5 | const messaging = @import("messaging.zig"); 6 | const WriteBuffer = messaging.WriteBuffer; 7 | const Md5 = std.crypto.hash.Md5; 8 | const Hmac = std.crypto.auth.hmac.sha2.HmacSha256; 9 | const Base64 = std.base64.standard; 10 | 11 | pub fn md5(user: []const u8, password: []const u8, salt: []const u8) [35]u8 { 12 | var digest: [16]u8 = undefined; 13 | var hasher = Md5.init(.{}); 14 | hasher.update(password); 15 | hasher.update(user); 16 | hasher.final(&digest); 17 | hasher = Md5.init(.{}); 18 | hasher.update(&hexDigest(digest)); 19 | hasher.update(salt); 20 | hasher.final(&digest); 21 | return ("md5" ++ hexDigest(digest)).*; 22 | } 23 | 24 | const hex_charset = "0123456789abcdef"; 25 | fn hexDigest(digest: [16]u8) [32]u8 { 26 | var ret: [32]u8 = undefined; 27 | for (digest, 0..) |byte, i| { 28 | ret[i * 2 + 0] = hex_charset[byte >> 4]; 29 | ret[i * 2 + 1] = hex_charset[byte & 15]; 30 | } 31 | return ret; 32 | } 33 | 34 | pub const Scram = struct { 35 | buffer: [512]u8 = undefined, 36 | state: State, 37 | 38 | pub const State = union(enum) { 39 | update: struct { 40 | nonce: [24]u8, 41 | password: []const u8, 42 | }, 43 | finish: struct { 44 | salted_password: [32]u8, 45 | auth: []const u8, 46 | message: []const u8, 47 | }, 48 | done: void, 49 | 50 | pub fn writeTo(self: *State, wb: *WriteBuffer) void { 51 | switch (self.*) { 52 | .update => |u| { 53 | const len = "n,,n=,r=".len + u.nonce.len; 54 | wb.writeString("SCRAM-SHA-256"); 55 | wb.writeInt(u32, @as(u32, @intCast(len))); 56 | wb.writeBytes("n,,n=,r="); 57 | wb.writeBytes(&u.nonce); 58 | }, 59 | .finish => |f| { 60 | wb.writeBytes(f.message); 61 | }, 62 | .done => {}, 63 | } 64 | } 65 | }; 66 | 67 | pub fn init(password: []const u8) Scram { 68 | var nonce: [24]u8 = undefined; 69 | var randomizer = std.rand.Xoshiro256.init(@as(u64, @intCast(std.time.milliTimestamp()))); 70 | for (&nonce) |*b| { 71 | var byte = randomizer.random().intRangeAtMost(u8, 0x21, 0x7e); 72 | if (byte == 0x2c) { 73 | byte = 0x7e; 74 | } 75 | b.* = byte; 76 | } 77 | 78 | return Scram{ 79 | .state = .{ 80 | .update = .{ 81 | .nonce = nonce, 82 | .password = password, 83 | }, 84 | }, 85 | }; 86 | } 87 | 88 | pub fn update(self: *Scram, message: []const u8) !void { 89 | if (std.meta.activeTag(self.state) != .update) return error.InvalidState; 90 | 91 | var nonce: []const u8 = ""; 92 | var salt: []const u8 = ""; 93 | var iterations: []const u8 = ""; 94 | 95 | var parser = std.mem.tokenize(u8, message, ","); 96 | while (parser.next()) |kv| { 97 | if (kv[0] == 'r' and kv.len > 2) { 98 | nonce = kv[2..]; 99 | } 100 | if (kv[0] == 's' and kv.len > 2) { 101 | salt = kv[2..]; 102 | } 103 | if (kv[0] == 'i' and kv.len > 2) { 104 | iterations = kv[2..]; 105 | } 106 | } 107 | if (nonce.len == 0 or salt.len == 0 or iterations.len == 0) { 108 | return error.InvalidInput; 109 | } 110 | 111 | if (!std.mem.startsWith(u8, nonce, &self.state.update.nonce)) { 112 | return error.InvalidInput; 113 | } 114 | 115 | var decoded_salt_buf: [32]u8 = undefined; 116 | const decoded_salt_len = try Base64.Decoder.calcSizeForSlice(salt); 117 | if (decoded_salt_len > 32) return error.OutOfMemory; 118 | try Base64.Decoder.decode(&decoded_salt_buf, salt); 119 | const decoded_salt = decoded_salt_buf[0..decoded_salt_len]; 120 | 121 | var salted_password = hi(self.state.update.password, decoded_salt, try std.fmt.parseInt(usize, iterations, 10)); 122 | var hmac = Hmac.init(&salted_password); 123 | hmac.update("Client Key"); 124 | var client_key: [Hmac.key_length]u8 = undefined; 125 | hmac.final(&client_key); 126 | 127 | var sha256 = std.crypto.hash.sha2.Sha256.init(.{}); 128 | sha256.update(&client_key); 129 | var stored_key = sha256.finalResult(); 130 | 131 | // base64 of 'n,,' 132 | const cbind = "biws"; 133 | 134 | var finish_state = Scram.State{ .finish = undefined }; 135 | 136 | finish_state.finish.auth = try std.fmt.bufPrint(self.buffer[0..256], "n=,r={s},{s},c={s},r={s}", .{ 137 | self.state.update.nonce, 138 | message, 139 | cbind, 140 | nonce, 141 | }); 142 | 143 | var client_hmac = Hmac.init(&stored_key); 144 | client_hmac.update(finish_state.finish.auth); 145 | var client_signature: [Hmac.key_length]u8 = undefined; 146 | client_hmac.final(&client_signature); 147 | 148 | var client_proof = client_key; 149 | var i: usize = 0; 150 | while (i < Hmac.key_length) : (i += 1) { 151 | client_proof[i] ^= client_signature[i]; 152 | } 153 | 154 | var encoded_proof: [Base64.Encoder.calcSize(Hmac.key_length)]u8 = undefined; 155 | _ = Base64.Encoder.encode(&encoded_proof, &client_proof); 156 | 157 | finish_state.finish.message = try std.fmt.bufPrint(self.buffer[256..512], "c={s},r={s},p={s}", .{ 158 | cbind, 159 | nonce, 160 | &encoded_proof, 161 | }); 162 | 163 | finish_state.finish.salted_password = salted_password; 164 | self.state = finish_state; 165 | } 166 | 167 | pub fn finish(self: *Scram, message: []const u8) !void { 168 | if (std.meta.activeTag(self.state) != .finish) return error.InvalidState; 169 | if (message[0] != 'v' and message.len <= 2) return error.InvalidInput; 170 | 171 | const verifier = message[2..]; 172 | var verifier_buf: [128]u8 = undefined; 173 | const verifier_len = try Base64.Decoder.calcSizeForSlice(verifier); 174 | if (verifier_len > 128) return error.OutOfMemory; 175 | try Base64.Decoder.decode(&verifier_buf, verifier); 176 | const decoded_verified = verifier_buf[0..verifier_len]; 177 | 178 | var hmac = Hmac.init(&self.state.finish.salted_password); 179 | hmac.update("Server Key"); 180 | var server_key: [32]u8 = undefined; 181 | hmac.final(&server_key); 182 | 183 | hmac = Hmac.init(&server_key); 184 | hmac.update(self.state.finish.auth); 185 | var hashed_verified: [Hmac.key_length]u8 = undefined; 186 | hmac.final(&hashed_verified); 187 | 188 | if (!std.mem.eql(u8, decoded_verified, &hashed_verified)) return error.VerifyError; 189 | 190 | self.state = .{ .done = {} }; 191 | } 192 | }; 193 | 194 | fn hi(string: []const u8, salt: []const u8, iterations: usize) [32]u8 { 195 | var result: [Hmac.key_length]u8 = undefined; 196 | 197 | var hmac = Hmac.init(string); 198 | hmac.update(salt); 199 | hmac.update(&.{ 0, 0, 0, 1 }); 200 | var previous: [Hmac.key_length]u8 = undefined; 201 | hmac.final(&previous); 202 | 203 | result = previous; 204 | 205 | for (1..iterations) |_| { 206 | var hmac_iter = Hmac.init(string); 207 | hmac_iter.update(&previous); 208 | hmac_iter.final(&previous); 209 | 210 | for (0..Hmac.key_length) |j| { 211 | result[j] ^= previous[j]; 212 | } 213 | } 214 | 215 | return result; 216 | } 217 | 218 | test "scram-sha-256" { 219 | const password = "foobar"; 220 | const nonce = "9IZ2O01zb9IgiIZ1WJ/zgpJB"; 221 | const client_first = "n,,n=,r=9IZ2O01zb9IgiIZ1WJ/zgpJB"; 222 | const server_first = "r=9IZ2O01zb9IgiIZ1WJ/zgpJBjx/oIRLs02gGSHcw1KEty3eY,s=fs3IXBy7U7+IvVjZ,i=4096"; 223 | const client_final = "c=biws,r=9IZ2O01zb9IgiIZ1WJ/zgpJBjx/oIRLs02gGSHcw1KEty3eY,p=AmNKosjJzS31NTlQYNs5BTeQjdHdk7lOflDo5re2an8="; 224 | const server_final = "v=U+ppxD5XUKtradnv8e2MkeupiA8FU87Sg8CXzXHDAzw="; 225 | 226 | var wb = try WriteBuffer.init(std.testing.allocator, 'p'); 227 | defer wb.deinit(); 228 | 229 | var scram = Scram.init(password); 230 | @memcpy(scram.state.update.nonce[0..nonce.len], nonce); 231 | scram.state.writeTo(&wb); 232 | try std.testing.expectEqualStrings(client_first, wb.buf.items[23..]); 233 | 234 | try scram.update(server_first); 235 | wb.reset('p'); 236 | scram.state.writeTo(&wb); 237 | try std.testing.expectEqualStrings(client_final, wb.buf.items[5..]); 238 | 239 | try scram.finish(server_final); 240 | } 241 | -------------------------------------------------------------------------------- /src/pgz.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const encdec = @import("encdec.zig"); 3 | const messaging = @import("messaging.zig"); 4 | const auth = @import("auth.zig"); 5 | 6 | const ReadBuffer = messaging.ReadBuffer; 7 | const WriteBuffer = messaging.WriteBuffer; 8 | const Message = messaging.Message; 9 | 10 | /// Quotes identifier, caller owns memory 11 | pub const quoteIdentifier = encdec.quoteIdentifier; 12 | /// Quotes literal, caller owns memory 13 | pub const quoteLiteral = encdec.quoteLiteral; 14 | 15 | /// Error returned from Postgres server 16 | pub const Error = struct { 17 | severity: [5]u8 = undefined, 18 | code: [5]u8 = undefined, 19 | message: [128]u8 = undefined, 20 | length: u32 = 0, 21 | 22 | pub const Severity = enum { 23 | ERROR, 24 | FATAL, 25 | PANIC, 26 | }; 27 | 28 | pub fn getSeverity(self: *const Error) Severity { 29 | return std.meta.stringToEnum(Severity, self.severity[0..]).?; 30 | } 31 | 32 | pub fn getCode(self: *const Error) []const u8 { 33 | return self.code[0..]; 34 | } 35 | 36 | pub fn getMessage(self: *const Error) []const u8 { 37 | return self.message[0..self.length]; 38 | } 39 | }; 40 | 41 | const RowHeader = struct { 42 | name: []const u8, 43 | type: u32, 44 | binary: bool, 45 | }; 46 | 47 | /// Query result that holds data, do not forget to `deinit` 48 | pub fn QueryResult(comptime T: type) type { 49 | return struct { 50 | allocator: std.mem.Allocator, 51 | data: []T, 52 | affectedRows: u32, 53 | 54 | /// Deinitializes allocated data. You can omit `deinit` if your data doesn't contain strings 55 | pub fn deinit(self: *@This()) void { 56 | for (self.data) |row| { 57 | inline for (@typeInfo(T).Struct.fields) |field| { 58 | if (@typeInfo(field.type) == .Pointer) { 59 | self.allocator.free(@field(row, field.name)); 60 | } 61 | if (@typeInfo(field.type) == .Optional and @typeInfo(@typeInfo(field.type).Optional.child) == .Pointer and @field(row, field.name) != null) { 62 | self.allocator.free(@field(row, field.name).?); 63 | } 64 | } 65 | } 66 | self.allocator.free(self.data); 67 | } 68 | }; 69 | } 70 | 71 | /// Single blocking Postgres connection 72 | pub const Connection = struct { 73 | allocator: std.mem.Allocator, 74 | stream: std.net.Stream, 75 | statement_count: u32 = 0, 76 | last_error: ?Error = null, 77 | 78 | /// Connect to Postgres server with DSN connection string 79 | /// example: `postgres://testing:testing@localhost:5432/testing` 80 | pub fn init(allocator: std.mem.Allocator, dsn: std.Uri) !Connection { 81 | var connection = Connection{ 82 | .allocator = allocator, 83 | .stream = undefined, 84 | }; 85 | const host = if (dsn.host) |h| try std.fmt.allocPrint(allocator, "{host}", .{h}) else try allocator.dupe(u8, "localhost"); 86 | defer allocator.free(host); 87 | const user = if (dsn.user) |u| try std.fmt.allocPrint(allocator, "{user}", .{u}) else try allocator.dupe(u8, "postgres"); 88 | defer allocator.free(user); 89 | const path = try std.fmt.allocPrintZ(allocator, "{path}", .{dsn.path}); 90 | defer allocator.free(path); 91 | 92 | const password = if (dsn.password) |pa| try std.fmt.allocPrint(allocator, "{password}", .{pa}) else try allocator.dupe(u8, ""); 93 | defer allocator.free(password); 94 | connection.stream = try std.net.tcpConnectToHost(allocator, host, dsn.port orelse 5432); 95 | try connection.startup(user, path[1..]); 96 | while (true) { 97 | var msg = try Message.read(allocator, connection.stream.reader()); 98 | defer msg.free(allocator); 99 | switch (msg.type) { 100 | 'R' => try connection.handleAuth(msg, user, password), 101 | 'K' => {}, // TODO: handle 102 | 'S' => {}, // TODO: handle 103 | 'Z' => break, 104 | else => return error.UnexpectedMessage, 105 | } 106 | } 107 | return connection; 108 | } 109 | 110 | /// Assumes that error catched in one of the connection methods 111 | /// exec, query, prepare, and corresponding statement methods 112 | pub fn getLastError(self: *Connection) Error { 113 | return self.last_error.?; 114 | } 115 | 116 | /// Executes SQL query. You can execute multiple queries in a row 117 | pub fn exec(self: *Connection, sql: []const u8) !void { 118 | var wb = try WriteBuffer.init(self.allocator, 'Q'); 119 | defer wb.deinit(); 120 | wb.writeString(sql); 121 | try wb.send(self.stream); 122 | 123 | while (true) { 124 | var msg = try Message.read(self.allocator, self.stream.reader()); 125 | defer msg.free(self.allocator); 126 | switch (msg.type) { 127 | 'E' => { 128 | self.parseError(msg); 129 | return error.SqlExecuteError; 130 | }, 131 | 'Z' => break, 132 | else => {}, 133 | } 134 | } 135 | } 136 | 137 | /// Executes single SQL query, and scans data into type `T` 138 | /// caller owns memory, release memory with `result.deinit()` function 139 | pub fn query(self: *Connection, sql: []const u8, comptime T: type) !QueryResult(T) { 140 | var wb = try WriteBuffer.init(self.allocator, 'Q'); 141 | defer wb.deinit(); 142 | wb.writeString(sql); 143 | try wb.send(self.stream); 144 | return self.fetchRows(T); 145 | } 146 | 147 | /// Prepares statement to safely bind values to SQL query 148 | /// caller owns memory, release memory with `statement.deinit()` 149 | pub fn prepare(self: *Connection, sql: []const u8) !Statement { 150 | var name_buffer: [10]u8 = undefined; // 4294967295 - max value - length 10 151 | const name = try std.fmt.bufPrint(&name_buffer, "{d}", .{self.statement_count}); 152 | self.statement_count += 1; 153 | 154 | var wb = try WriteBuffer.init(self.allocator, 'P'); 155 | defer wb.deinit(); 156 | wb.writeString(name); 157 | wb.writeString(sql); 158 | wb.writeInt(u16, 0); 159 | wb.next('D'); 160 | wb.writeInt(u8, 'S'); 161 | wb.writeString(name); 162 | wb.next('S'); 163 | try wb.send(self.stream); 164 | 165 | while (true) { 166 | var msg = try Message.read(self.allocator, self.stream.reader()); 167 | defer msg.free(self.allocator); 168 | switch (msg.type) { 169 | 'E' => { 170 | self.parseError(msg); 171 | return error.SqlPrepareError; 172 | }, 173 | 'Z' => break, 174 | else => {}, 175 | } 176 | } 177 | 178 | return Statement{ .connection = self.*, .statement = self.statement_count - 1 }; 179 | } 180 | 181 | /// Closes and frees memory owned by Connection 182 | pub fn deinit(self: *Connection) void { 183 | self.notifyClose() catch {}; 184 | self.stream.close(); 185 | } 186 | 187 | fn startup(self: *Connection, user: []const u8, database: []const u8) !void { 188 | var wb = try WriteBuffer.init(self.allocator, null); 189 | defer wb.deinit(); 190 | wb.writeInt(u32, 196608); 191 | wb.writeString("user"); 192 | wb.writeString(user); 193 | wb.writeString("database"); 194 | wb.writeString(database); 195 | wb.writeInt(u8, 0); 196 | try wb.send(self.stream); 197 | } 198 | 199 | fn handleAuth(self: *Connection, msg: Message, user: []const u8, password: []const u8) !void { 200 | if (msg.type != 'R') return error.UnexpectedMessage; 201 | 202 | var buffer = ReadBuffer.init(msg.msg); 203 | const password_type = buffer.readInt(u32); 204 | switch (password_type) { 205 | 0 => {}, 206 | 5 => { 207 | const salt = buffer.readBytes(4); 208 | 209 | var md5 = auth.md5(user, password, salt); 210 | var wb = try WriteBuffer.init(self.allocator, 'p'); 211 | defer wb.deinit(); 212 | wb.writeString(&md5); 213 | try wb.send(self.stream); 214 | 215 | var check_msg = try Message.read(self.allocator, self.stream.reader()); 216 | defer check_msg.free(self.allocator); 217 | var check_buffer = ReadBuffer.init(check_msg.msg); 218 | const status = check_buffer.readInt(u32); 219 | if (status != 0) return error.AuthenticationError; 220 | }, 221 | 10 => { 222 | var scram = auth.Scram.init(password); 223 | 224 | var wb = try WriteBuffer.init(self.allocator, 'p'); 225 | defer wb.deinit(); 226 | scram.state.writeTo(&wb); 227 | try wb.send(self.stream); 228 | 229 | var server_first = try Message.read(self.allocator, self.stream.reader()); 230 | defer server_first.free(self.allocator); 231 | if (server_first.type != 'R') return error.AuthenticationError; 232 | scram.update(server_first.msg[4..]) catch return error.AuthenticationError; 233 | 234 | wb.reset('p'); 235 | scram.state.writeTo(&wb); 236 | try wb.send(self.stream); 237 | 238 | var server_final = try Message.read(self.allocator, self.stream.reader()); 239 | defer server_final.free(self.allocator); 240 | if (server_final.type != 'R') return error.AuthenticationError; 241 | scram.finish(server_final.msg[4..]) catch return error.AuthenticationError; 242 | }, 243 | else => return error.AuthenticationError, // TODO: handle other auth methods 244 | } 245 | } 246 | 247 | fn fetchRows(self: *Connection, comptime T: type) !QueryResult(T) { 248 | var affectedRows: u32 = 0; 249 | 250 | var row_headers = try std.ArrayListUnmanaged(RowHeader).initCapacity(self.allocator, 10); 251 | defer { 252 | for (row_headers.items) |row_header| { 253 | self.allocator.free(row_header.name); 254 | } 255 | row_headers.clearAndFree(self.allocator); 256 | } 257 | 258 | var result = try std.ArrayListUnmanaged(T).initCapacity(self.allocator, 10); 259 | defer result.deinit(self.allocator); 260 | 261 | while (true) { 262 | var msg = try Message.read(self.allocator, self.stream.reader()); 263 | defer msg.free(self.allocator); 264 | switch (msg.type) { 265 | 'C' => { 266 | if (msg.len > 4) { 267 | var buffer = ReadBuffer.init(msg.msg); 268 | affectedRows = parseAffectedRows(buffer.readString()); 269 | } 270 | }, 271 | 'E' => { 272 | self.parseError(msg); 273 | return error.SqlQueryError; 274 | }, 275 | 'Z' => break, 276 | 'T' => { 277 | var buffer = ReadBuffer.init(msg.msg); 278 | const num_rows = buffer.readInt(u16); 279 | try row_headers.ensureTotalCapacity(self.allocator, num_rows); 280 | for (0..num_rows) |_| { 281 | const name = try self.allocator.dupe(u8, buffer.readString()); 282 | _ = buffer.readInt(u32); 283 | _ = buffer.readInt(u16); 284 | const data_type = buffer.readInt(u32); 285 | _ = buffer.readInt(u16); 286 | _ = buffer.readInt(u32); 287 | const text_or_binary = buffer.readInt(u16); 288 | try row_headers.append(self.allocator, RowHeader{ 289 | .name = name, 290 | .type = data_type, 291 | .binary = text_or_binary == 1, 292 | }); 293 | } 294 | }, 295 | 'D' => { 296 | var buffer = ReadBuffer.init(msg.msg); 297 | const num_rows = buffer.readInt(u16); 298 | var row: T = undefined; 299 | 300 | for (0..num_rows) |i| { 301 | const len = buffer.readInt(u32); 302 | var value: ?[]const u8 = undefined; 303 | if (len == @as(u32, @truncate(-1))) { 304 | value = null; 305 | } else { 306 | value = buffer.readBytes(len); 307 | } 308 | 309 | if (@typeInfo(T).Struct.is_tuple) { 310 | inline for (@typeInfo(T).Struct.fields, 0..) |field, j| { 311 | if (i == j) { 312 | @field(row, field.name) = try encdec.decode(self.allocator, value, field.type); 313 | } 314 | } 315 | } else { 316 | inline for (@typeInfo(T).Struct.fields, 0..) |field, j| { 317 | if (i == j and std.mem.eql(u8, row_headers.items[i].name, field.name)) { 318 | @field(row, field.name) = try encdec.decode(self.allocator, value, field.type); 319 | } 320 | } 321 | } 322 | } 323 | 324 | try result.append(self.allocator, row); 325 | }, 326 | else => {}, 327 | } 328 | } 329 | return QueryResult(T){ 330 | .allocator = self.allocator, 331 | .data = try result.toOwnedSlice(self.allocator), 332 | .affectedRows = affectedRows, 333 | }; 334 | } 335 | 336 | fn parseError(self: *Connection, msg: Message) void { 337 | self.last_error = Error{}; 338 | 339 | var rb = ReadBuffer.init(msg.msg); 340 | var code = rb.readInt(u8); 341 | while (code != 0) : (code = rb.readInt(u8)) { 342 | switch (code) { 343 | 'S', 'V' => { 344 | const s = rb.readString(); 345 | @memcpy(self.last_error.?.severity[0..s.len], s); 346 | }, 347 | 'C' => { 348 | const s = rb.readString(); 349 | @memcpy(self.last_error.?.code[0..s.len], s); 350 | }, 351 | 'M' => { 352 | const message = rb.readString(); 353 | if (message.len > 256) { 354 | self.last_error.?.length = 0; 355 | } else { 356 | self.last_error.?.length = @as(u32, @intCast(message.len)); 357 | @memcpy(self.last_error.?.message[0..message.len], message); 358 | } 359 | }, 360 | else => { 361 | _ = rb.readString(); 362 | }, 363 | } 364 | } 365 | } 366 | 367 | fn notifyClose(self: *Connection) !void { 368 | var wb = try WriteBuffer.init(self.allocator, 'X'); 369 | defer wb.deinit(); 370 | try wb.send(self.stream); 371 | } 372 | }; 373 | 374 | /// Prepared statement binded to Connection 375 | pub const Statement = struct { 376 | connection: Connection, 377 | statement: u32, 378 | 379 | /// deinitializes and frees allocated memory 380 | pub fn deinit(self: *Statement) void { 381 | var name_buffer: [10]u8 = undefined; // 4294967295 - max value - length 10 382 | const name = std.fmt.bufPrint(&name_buffer, "{d}", .{self.statement}) catch return; 383 | var buffer = WriteBuffer.init(self.connection.allocator, 'C') catch return; 384 | defer buffer.deinit(); 385 | buffer.writeInt(u8, 'C'); 386 | buffer.writeString(name); 387 | buffer.next('S'); 388 | buffer.send(self.connection.stream) catch return; 389 | } 390 | 391 | /// Binds `args` to statement and executes query 392 | pub fn exec(self: *Statement, args: anytype) !void { 393 | try self.sendExec(args); 394 | 395 | while (true) { 396 | var msg = try Message.read(self.connection.allocator, self.connection.stream.reader()); 397 | defer msg.free(self.connection.allocator); 398 | switch (msg.type) { 399 | 'E' => { 400 | self.connection.parseError(msg); 401 | return error.SqlExecuteError; 402 | }, 403 | 'Z' => break, 404 | else => {}, 405 | } 406 | } 407 | } 408 | 409 | /// Binds `args` to statement, executes single SQL query and scans data into type `T` 410 | /// caller owns memory, release memory with `result.deinit()` function 411 | pub fn query(self: *Statement, comptime T: type, args: anytype) !QueryResult(T) { 412 | try self.sendExec(args); 413 | return try self.connection.fetchRows(T); 414 | } 415 | 416 | fn sendExec(self: *Statement, args: anytype) !void { 417 | var name_buffer: [10]u8 = undefined; // 4294967295 - max value - length 10 418 | const name = try std.fmt.bufPrint(&name_buffer, "{d}", .{self.statement}); 419 | 420 | var wb = try WriteBuffer.init(self.connection.allocator, 'B'); 421 | defer wb.deinit(); 422 | wb.writeInt(u8, 0); 423 | wb.writeString(name); 424 | wb.writeInt(u16, 0); 425 | wb.writeInt(u16, args.len); 426 | inline for (@typeInfo(@TypeOf(args)).Struct.fields) |field| { 427 | if ((@typeInfo(field.type) == .Optional or @typeInfo(field.type) == .Null) and @field(args, field.name) == null) { 428 | wb.writeInt(u32, @as(u32, @truncate(-1))); 429 | } else { 430 | const encoded = try encdec.encode(self.connection.allocator, @field(args, field.name)); 431 | defer self.connection.allocator.free(encoded); 432 | wb.writeInt(u32, @as(u32, @intCast(encoded.len))); 433 | wb.writeBytes(encoded); 434 | } 435 | } 436 | wb.writeInt(u16, 0); 437 | wb.next('E'); 438 | wb.writeInt(u8, 0); 439 | wb.writeInt(u32, 0); 440 | wb.next('S'); 441 | try wb.send(self.connection.stream); 442 | } 443 | }; 444 | 445 | fn parseAffectedRows(command: []const u8) u32 { 446 | if (command.len == 0) return 0; 447 | 448 | var tokenizer = std.mem.tokenize(u8, command, " "); 449 | _ = tokenizer.next(); // INSERT or SELECT 450 | const second = tokenizer.next(); // 0 or affected rows 451 | const maybe_last = tokenizer.next(); // affected rows or EOF 452 | if (maybe_last) |last| { 453 | return std.fmt.parseInt(u32, last, 10) catch 0; 454 | } else { 455 | return std.fmt.parseInt(u32, second.?, 10) catch 0; 456 | } 457 | } 458 | 459 | test "connect" { 460 | var conn = try Connection.init(std.testing.allocator, try std.Uri.parse("postgres://testing:testing@localhost:5432/testing?sslmode=disable")); 461 | defer conn.deinit(); 462 | } 463 | 464 | test "wrong auth" { 465 | const res = Connection.init(std.testing.allocator, try std.Uri.parse("postgres://testing:wrong@localhost:5432/testing")); 466 | try std.testing.expectError(error.AuthenticationError, res); 467 | } 468 | 469 | test "exec" { 470 | var conn = try Connection.init(std.testing.allocator, try std.Uri.parse("postgres://testing:testing@localhost:5432/testing")); 471 | defer conn.deinit(); 472 | try conn.exec("SELECT 1;"); 473 | } 474 | 475 | test "query" { 476 | var conn = try Connection.init(std.testing.allocator, try std.Uri.parse("postgres://testing:testing@localhost:5432/testing")); 477 | defer conn.deinit(); 478 | var result = try conn.query("SELECT 1;", struct { u8 }); 479 | defer result.deinit(); 480 | try std.testing.expectEqual(@as(usize, 1), result.data.len); 481 | try std.testing.expectEqual(@as(u8, 1), result.data[0].@"0"); 482 | } 483 | 484 | test "query named" { 485 | var conn = try Connection.init(std.testing.allocator, try std.Uri.parse("postgres://testing:testing@localhost:5432/testing")); 486 | defer conn.deinit(); 487 | var result = try conn.query("SELECT 1 as number;", struct { number: u8 }); 488 | defer result.deinit(); 489 | try std.testing.expectEqual(@as(usize, 1), result.data.len); 490 | try std.testing.expectEqual(@as(u8, 1), result.data[0].number); 491 | } 492 | 493 | test "query multiple rows" { 494 | var conn = try Connection.init(std.testing.allocator, try std.Uri.parse("postgres://testing:testing@localhost:5432/testing")); 495 | defer conn.deinit(); 496 | var result = try conn.query("VALUES (1,2,3), (4,5,6), (7,8,9);", struct { u8, u8, u8 }); 497 | defer result.deinit(); 498 | try std.testing.expectEqual(@as(usize, 3), result.data.len); 499 | try std.testing.expectEqual(@as(u8, 1), result.data[0].@"0"); 500 | try std.testing.expectEqual(@as(u8, 4), result.data[1].@"0"); 501 | try std.testing.expectEqual(@as(u8, 7), result.data[2].@"0"); 502 | } 503 | 504 | test "query multiple named rows" { 505 | var conn = try Connection.init(std.testing.allocator, try std.Uri.parse("postgres://testing:testing@localhost:5432/testing")); 506 | defer conn.deinit(); 507 | var result = try conn.query("VALUES (1,2,3), (4,5,6), (7,8,9);", struct { column1: u8, column2: u8, column3: u8 }); 508 | defer result.deinit(); 509 | try std.testing.expectEqual(@as(usize, 3), result.data.len); 510 | try std.testing.expectEqual(@as(u8, 1), result.data[0].column1); 511 | try std.testing.expectEqual(@as(u8, 5), result.data[1].column2); 512 | try std.testing.expectEqual(@as(u8, 9), result.data[2].column3); 513 | } 514 | 515 | test "prepare" { 516 | var conn = try Connection.init(std.testing.allocator, try std.Uri.parse("postgres://testing:testing@localhost:5432/testing")); 517 | defer conn.deinit(); 518 | var stmt = try conn.prepare("SELECT 1 + $1;"); 519 | defer stmt.deinit(); 520 | var result = try stmt.query(struct { []const u8 }, .{2}); 521 | defer result.deinit(); 522 | try std.testing.expectEqual(@as(usize, 1), result.data.len); 523 | try std.testing.expectEqualStrings("3", result.data[0].@"0"); 524 | } 525 | 526 | test "prepare exec many times" { 527 | var conn = try Connection.init(std.testing.allocator, try std.Uri.parse("postgres://testing:testing@localhost:5432/testing")); 528 | defer conn.deinit(); 529 | var stmt = try conn.prepare("SELECT 1 + $1;"); 530 | defer stmt.deinit(); 531 | try stmt.exec(.{1}); 532 | try stmt.exec(.{2}); 533 | try stmt.exec(.{3}); 534 | } 535 | 536 | test "encoding decoding null" { 537 | var conn = try Connection.init(std.testing.allocator, try std.Uri.parse("postgres://testing:testing@localhost:5432/testing")); 538 | defer conn.deinit(); 539 | var stmt = try conn.prepare("SELECT $1, $2;"); 540 | defer stmt.deinit(); 541 | const a: ?u32 = null; 542 | const b: ?[]const u8 = "hi"; 543 | var result = try stmt.query(struct { ?u8, ?[]const u8 }, .{ a, b }); 544 | defer result.deinit(); 545 | try std.testing.expectEqual(@as(usize, 1), result.data.len); 546 | try std.testing.expectEqual(@as(?u8, null), result.data[0].@"0"); 547 | try std.testing.expectEqualStrings("hi", result.data[0].@"1".?); 548 | } 549 | 550 | test "get last error" { 551 | var conn = try Connection.init(std.testing.allocator, try std.Uri.parse("postgres://testing:testing@localhost:5432/testing")); 552 | defer conn.deinit(); 553 | conn.exec("SELECT 1/0;") catch { 554 | try std.testing.expectEqual(Error.Severity.ERROR, conn.getLastError().getSeverity()); 555 | try std.testing.expectEqualStrings("division by zero", conn.getLastError().getMessage()); 556 | }; 557 | } 558 | --------------------------------------------------------------------------------