authorgravatar for hello@nektro.netMeghan Denny <hello@nektro.net> 2026-05-21 04:10:18 -07:00
committergravatar for hello@nektro.netMeghan Denny <hello@nektro.net> 2026-05-21 04:10:18 -07:00
log2eb196d362acac424242f55d806adfdf33123d88
treea742a538bf0fbfc08b3e8a9da01fabd21c7eb6d9
parent4e148236e27c1371109d9c1d01ad02858859f519
signaturebadge-check Signed by SSH key SHA256:4hHJbtBRU58AYXwjL7fkz2fnQHdiye8x1QpTCQ0sHNw

start postgresql


5 files changed, 441 insertions(+), 3 deletions(-)

licenses.txt+4
...@@ -1,8 +1,11 @@...@@ -1,8 +1,11 @@
1MPL-2.0:1MPL-2.0:
2= https://spdx.org/licenses/MPL-2.02= https://spdx.org/licenses/MPL-2.0
3- This3- This
4- git https://github.com/nektro/zig-net
4- git https://github.com/nektro/zig-nfs5- git https://github.com/nektro/zig-nfs
5- git https://github.com/nektro/zig-nio6- git https://github.com/nektro/zig-nio
7- git https://github.com/nektro/zig-unicode-idna
8- git https://github.com/nektro/zig-whatwg-url
69
7MIT:10MIT:
8= https://spdx.org/licenses/MIT11= https://spdx.org/licenses/MIT
...@@ -10,6 +13,7 @@ MIT:...@@ -10,6 +13,7 @@ MIT:
10- git https://github.com/nektro/zig-sys-linux13- git https://github.com/nektro/zig-sys-linux
11- git https://github.com/nektro/zig-time14- git https://github.com/nektro/zig-time
12- git https://github.com/nektro/zig-tracer15- git https://github.com/nektro/zig-tracer
16- git https://github.com/nektro/zig-unicode-ucd
13- git https://github.com/vrischmann/zig-sqlite17- git https://github.com/vrischmann/zig-sqlite
1418
15blessing:19blessing:
src/lib.zig+8-2
...@@ -2,22 +2,25 @@ const std = @import("std");...@@ -2,22 +2,25 @@ const std = @import("std");
22
3pub const DriverType = enum {3pub const DriverType = enum {
4 sqlite3,4 sqlite3,
5 postgresql,
5};6};
67
7pub fn Driver(comptime etype: DriverType) type {8pub fn Driver(comptime etype: DriverType) type {
8 return switch (etype) {9 return switch (etype) {
9 .sqlite3 => @import("./sqlite3.zig"),10 .sqlite3 => @import("./sqlite3.zig"),
11 .postgresql => @import("./postgresql.zig"),
10 };12 };
11}13}
1214
13pub fn connect(_type: DriverType, allocator: std.mem.Allocator, connection: [:0]const u8) !Engine {15pub fn connect(_type: DriverType, allocator: std.mem.Allocator, connection: [:0]const u8) !Engine {
14 return switch (_type) {16 return switch (_type) {
15 .sqlite3 => .{ .sqlite3 = try .connect(allocator, connection) },17 inline else => |t| @unionInit(Engine, @tagName(t), try .connect(allocator, connection)),
16 };18 };
17}19}
1820
19pub const Engine = union(DriverType) {21pub const Engine = union(DriverType) {
20 sqlite3: Driver(.sqlite3),22 sqlite3: Driver(.sqlite3),
23 postgresql: Driver(.postgresql),
2124
22 pub fn close(engine: *Engine) void {25 pub fn close(engine: *Engine) void {
23 return switch (engine.*) {26 return switch (engine.*) {
...@@ -57,7 +60,7 @@ pub const Engine = union(DriverType) {...@@ -57,7 +60,7 @@ pub const Engine = union(DriverType) {
5760
58 pub fn collectDyn(engine: *Engine, alloc: std.mem.Allocator, comptime T: type, query: []const u8, args: anytype) ![]T {61 pub fn collectDyn(engine: *Engine, alloc: std.mem.Allocator, comptime T: type, query: []const u8, args: anytype) ![]T {
59 return switch (engine.*) {62 return switch (engine.*) {
60 inline else => |*e| {63 .sqlite3 => |*e| {
61 var stmt = try e.prepareDynamic(query);64 var stmt = try e.prepareDynamic(query);
62 defer stmt.deinit();65 defer stmt.deinit();
63 var iter = try stmt.iteratorAlloc(T, alloc, args);66 var iter = try stmt.iteratorAlloc(T, alloc, args);
...@@ -68,6 +71,9 @@ pub const Engine = union(DriverType) {...@@ -68,6 +71,9 @@ pub const Engine = union(DriverType) {
68 }71 }
69 return list.toOwnedSlice();72 return list.toOwnedSlice();
70 },73 },
74 .postgresql => {
75 @panic("TODO");
76 },
71 };77 };
72 }78 }
7379
src/postgresql.zig created+422
...@@ -0,0 +1,422 @@
1//! Reference: https://www.postgresql.org/docs/current/protocol.html
2//! Implements version 3.0 (PostgreSQL 7.4 and later)
3
4const std = @import("std");
5const builtin = @import("builtin");
6const url = @import("url");
7const net = @import("net");
8const nio = @import("nio");
9const extras = @import("extras");
10const tracer = @import("tracer");
11
12const sys = switch (builtin.target.os.tag) {
13 .linux => @import("sys-linux"),
14 else => unreachable,
15};
16
17const Driver = @This();
18
19mutex: std.Thread.Mutex,
20conn: net.Stream,
21bufw: nio.BufferedWriter(4096, net.Stream),
22bufr: nio.BufferedReader(4096, net.Stream),
23
24pub fn connect(allocator: std.mem.Allocator, connect_s: [:0]const u8) !Driver {
25 std.log.scoped(.zorm).info("connecting to {s} @ {s}", .{ "postgresql", connect_s });
26 const connect_url = try url.URL.parse(allocator, connect_s, null);
27 defer allocator.free(connect_url.href);
28
29 const addr: net.Address = try .fromUrl(&connect_url, allocator);
30 const conn = try addr.tcpConnect();
31 errdefer conn.close();
32
33 var bufw: nio.BufferedWriter(4096, net.Stream) = .init(conn);
34 _ = &bufw;
35
36 var bufr: nio.BufferedReader(4096, net.Stream) = .init(conn);
37 _ = &bufr;
38
39 // https://github.com/postgres/postgres/blob/REL_18_0/src/include/common/scram-common.h#L32-L37
40 const nonce_len = 18;
41 var cnonce_buf: [nonce_len]u8 = @splat(0);
42 var cnonce = try sys.getrandom(&cnonce_buf, 0);
43 _ = &cnonce;
44
45 var snonce_b64_buf: [128]u8 = @splat(0);
46 var snonce_b64: []u8 = snonce_b64_buf[0..];
47
48 var scram_i_buf: [16]u8 = @splat(0);
49 var scram_i_s: []u8 = scram_i_buf[0..];
50 var scram_i: u32 = 0;
51
52 const Base64Enc = std.base64.standard.Encoder;
53 const Base64Dec = std.base64.standard.Decoder;
54
55 {
56 try proto.StartupMessage.write(
57 &bufw,
58 connect_url.username,
59 connect_url.pathname[1..],
60 );
61 try bufw.flush();
62 }
63 {
64 const t: BackendMessageType = @enumFromInt(try bufr.readByte());
65 std.debug.assert(t == .Authentication);
66 const auth_len = try bufr.readInt(u32, .big);
67 std.log.warn("auth_len={d}", .{auth_len});
68 const auth = try bufr.readInt(u32, .big);
69 std.log.warn("auth={d}", .{auth});
70 switch (auth) {
71 10 => { // AuthenticationSASL
72 const methods = try bufr.readAlloc(allocator, auth_len - 4 - 4);
73 defer allocator.free(methods);
74 var methods_iter = std.mem.splitScalar(u8, methods, '\x00');
75
76 while (methods_iter.next()) |method| {
77 const method_z = method.ptr[0..method.len :0];
78 if (method_z.len == 0) break;
79 std.log.warn("method={s}", .{method_z});
80
81 // https://datatracker.ietf.org/doc/html/rfc7677
82 // https://datatracker.ietf.org/doc/html/rfc5802
83 // https://datatracker.ietf.org/doc/html/rfc4422
84 // Salted Challenge Response Authentication Mechanism (SCRAM) SASL and GSS-API Mechanisms
85
86 const mechanisms = [_]struct { []const u8, type }{
87 .{ "SCRAM-SHA-256", std.crypto.hash.sha2.Sha256 },
88 .{ "SCRAM-SHA-1", std.crypto.hash.Sha1 },
89 };
90 inline for (&mechanisms) |mechanism| {
91 const name, const Hash = mechanism;
92 const Hmac = std.crypto.auth.hmac.Hmac(Hash);
93 comptime std.debug.assert(Hash.digest_length == Hmac.mac_length);
94
95 if (std.mem.eql(u8, method_z, name)) {
96 // This is a simple example of a SCRAM-SHA-256 authentication exchange when the client doesn't support channel bindings.
97 // The username 'user' and password 'pencil' are being used.
98 // C: n,,n=user,r=rOprNGfwEbeRWgbNEkqO
99 // S: r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0,s=W22ZaJ0SNY7soEsUEjb6gQ==,i=4096
100 // C: c=biws,r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0,p=dHzbZapWIk4jUhN+Ute9ytag9zjfMHgsqmmiz7AndVQ=
101 // S: v=6rriTRBi23WpRR/wtup+mMhUZUn/dB5nLTJRsjl95G4=
102
103 // SaltedPassword := Hi(Normalize(password), salt, i)
104 // ClientKey := HMAC(SaltedPassword, "Client Key")
105 // StoredKey := H(ClientKey)
106 // AuthMessage := client-first-message-bare + "," + server-first-message + "," + client-final-message-without-proof
107 // ClientSignature := HMAC(StoredKey, AuthMessage)
108 // ClientProof := ClientKey XOR ClientSignature
109 // ServerKey := HMAC(SaltedPassword, "Server Key")
110 // ServerSignature := HMAC(ServerKey, AuthMessage)
111
112 // server-final-message = (verifier / server-error) ["," extensions]
113 // verifier = "v=" base64
114 // server-error = "e=" server-error-value
115 // server-error-value = "invalid-encoding" / "extensions-not-supported" / "invalid-proof" / "channel-bindings-dont-match" / "server-does-support-channel-binding" / "channel-binding-not-supported" / "unsupported-channel-binding-type" / "unknown-user" / "invalid-username-encoding" / "no-resources" / "other-error" / server-error-value-ext
116
117 var cnonce_b64_buf: [Base64Enc.calcSize(nonce_len)]u8 = @splat(0);
118 const cnonce_b64 = Base64Enc.encode(&cnonce_b64_buf, cnonce);
119
120 var salt_b64_buf: [128]u8 = @splat(0);
121 var salt_b64: []u8 = salt_b64_buf[0..];
122
123 var salt_buf: [128]u8 = @splat(0);
124 var salt_dec: []u8 = salt_buf[0..];
125
126 { //->SASLInitialResponse (scram client-first-message)
127
128 try proto.SASLInitialResponse.writev(
129 &bufw,
130 "SCRAM-SHA-256",
131 &.{
132 "n", "",
133 ",", "",
134 ",n=", connect_url.username,
135 ",r=", cnonce_b64,
136 },
137 );
138 try bufw.flush();
139 }
140 { //<-AuthenticationSASLContinue (scram server-first-message)
141 const t2: BackendMessageType = @enumFromInt(try bufr.readByte());
142 if (t2 == .ErrorResponse) try printError(&bufr, allocator);
143 std.debug.assert(t2 == .Authentication);
144 const len = try bufr.readInt(u32, .big);
145 const ok = try bufr.readInt(u32, .big);
146 std.debug.assert(ok == 11);
147 const data = try bufr.readAlloc(allocator, len - 8);
148 defer allocator.free(data);
149 var iter = std.mem.splitScalar(u8, data, ',');
150 while (iter.next()) |field| {
151 if (extras.trimPrefixEnsure(field, "r=")) |r| {
152 snonce_b64 = snonce_b64_buf[0..r.len];
153 @memcpy(snonce_b64, r);
154 }
155 if (extras.trimPrefixEnsure(field, "s=")) |s| {
156 salt_b64 = salt_b64_buf[0..s.len];
157 @memcpy(salt_b64, s);
158 const s_len = try Base64Dec.calcSizeForSlice(s);
159 salt_dec = salt_buf[0..s_len];
160 try Base64Dec.decode(salt_dec, s);
161 }
162 if (extras.trimPrefixEnsure(field, "i=")) |i| {
163 scram_i_s = scram_i_buf[0..i.len];
164 @memcpy(scram_i_s, i);
165 scram_i = extras.parseDigits(u32, i, 10) catch 0;
166 if (scram_i < 4096) return error.WeakParameters;
167 }
168 }
169 }
170 { //->SASLResponse (scram client-final-message)
171 var buf: [1024]u8 = @splat(0);
172 var fbs: nio.FixedBufferStream([]u8) = .init(&buf);
173 try fbs.writeAll("c=biws");
174 try fbs.writeAll(",r=");
175 try fbs.writeAll(snonce_b64);
176
177 const salted_password = hi(Hmac, connect_url.password, salt_dec, scram_i);
178 const client_key = hmac(Hmac, &salted_password, "Client Key");
179 const auth_messagev: []const []const u8 = &.{ "n=", connect_url.username, ",r=", cnonce_b64, ",", "r=", snonce_b64, ",s=", salt_b64, ",i=", scram_i_s, ",", fbs.written() };
180 const stored_key = h(Hash, &client_key);
181 const client_signature = hmacv(Hmac, &stored_key, auth_messagev);
182 const client_proof = xor(client_key, client_signature);
183 try fbs.writeAll(",p=");
184 try Base64Enc.encodeWriter(&fbs, &client_proof);
185
186 try proto.SASLResponse.write(
187 &bufw,
188 fbs.written(),
189 );
190 try bufw.flush();
191 }
192 { //<-AuthenticationSASLFinal (scram server-final-message)
193 const t2: BackendMessageType = @enumFromInt(try bufr.readByte());
194 if (t2 == .ErrorResponse) try printError(&bufr, allocator);
195 std.debug.assert(t2 == .Authentication);
196 const len = try bufr.readInt(u32, .big);
197 const ok = try bufr.readInt(u32, .big);
198 std.debug.assert(ok == 12);
199 const data = try bufr.readAlloc(allocator, len - 8);
200 defer allocator.free(data);
201 std.log.warn("AuthenticationSASLFinal = {s}", .{data});
202 }
203 { //<-AuthenticationOk
204 const t2: BackendMessageType = @enumFromInt(try bufr.readByte());
205 if (t2 == .ErrorResponse) try printError(&bufr, allocator);
206 std.debug.assert(t2 == .Authentication);
207 const len = try bufr.readInt(u32, .big);
208 std.debug.assert(len == 8);
209 const ok = try bufr.readInt(u32, .big);
210 std.debug.assert(ok == 0);
211 std.log.warn("AuthenticationOk", .{});
212 }
213 }
214 }
215 }
216 },
217 else => unreachable, // TODO
218 }
219 }
220
221 return .{
222 .mutex = .{},
223 .conn = conn,
224 .bufw = bufw,
225 .bufr = bufr,
226 };
227}
228
229pub fn close(driver: *Driver) void {
230 driver.conn.close();
231}
232
233pub fn lock(driver: *Driver) void {
234 driver.mutex.lock();
235}
236
237pub fn unlock(driver: *Driver) void {
238 driver.mutex.unlock();
239}
240
241//
242
243pub fn exec(driver: *Driver, alloc: std.mem.Allocator, comptime query: []const u8, args: anytype) !void {
244 _ = driver;
245 _ = alloc;
246 _ = query;
247 _ = args;
248 @panic("TODO");
249}
250
251pub fn first(driver: *Driver, alloc: std.mem.Allocator, comptime T: type, comptime query: []const u8, args: anytype) !?T {
252 _ = driver;
253 _ = alloc;
254 _ = query;
255 _ = args;
256 @panic("TODO");
257}
258
259pub fn collect(driver: *Driver, alloc: std.mem.Allocator, comptime T: type, comptime query: []const u8, args: anytype) ![]T {
260 _ = driver;
261 _ = alloc;
262 _ = query;
263 _ = args;
264 @panic("TODO");
265}
266
267//
268
269pub fn doesTableExist(driver: *Driver, alloc: std.mem.Allocator, name: []const u8) !bool {
270 _ = driver;
271 _ = alloc;
272 _ = name;
273 @panic("TODO");
274}
275
276pub fn hasColumnWithName(driver: *Driver, alloc: std.mem.Allocator, comptime table: []const u8, comptime column: []const u8) !bool {
277 _ = driver;
278 _ = alloc;
279 _ = table;
280 _ = column;
281 @panic("TODO");
282}
283
284//
285// CancelRequest
286// GSSENCRequest
287// SSLRequest
288// StartupMessage
289
290pub const BackendMessageType = enum(u8) {
291 Authentication = 'R',
292 BackendKeyData = 'K',
293 BindComplete = '2',
294 CloseComplete = '3',
295 CommandComplete = 'C',
296 CopyData = 'd',
297 CopyDone = 'c',
298 CopyInResponse = 'G',
299 CopyOutResponse = 'H',
300 CopyBothResponse = 'W',
301 DataRow = 'D',
302 EmptyQueryResponse = 'I',
303 ErrorResponse = 'E',
304 FunctionCallResponse = 'V',
305 NegotiateProtocolVersion = 'v',
306 NoData = 'n',
307 NoticeResponse = 'N',
308 NotificationResponse = 'A',
309 ParameterDescription = 't',
310 ParameterStatus = 'S',
311 ParseComplete = '1',
312 PortalSuspended = 's',
313 ReadyForQuery = 'Z',
314 RowDescription = 'T',
315};
316
317pub const FrontendMessageType = enum(u8) {
318 Bind = 'B',
319 Close = 'C',
320 CopyData = 'd',
321 CopyDone = 'c',
322 CopyFail = 'f',
323 Describe = 'D',
324 Execute = 'E',
325 Flush = 'H',
326 FunctionCall = 'F',
327 GSSResponse = 'p',
328 Parse = 'P',
329 PasswordMessage = 'p',
330 Query = 'Q',
331 // SASLInitialResponse = 'p',
332 // SASLResponse = 'p',
333 Sync = 'S',
334 Terminate = 'X',
335};
336
337const proto = struct {
338 const StartupMessage = struct {
339 fn write(writer: anytype, username: []const u8, database: []const u8) !void {
340 const length: u32 = 4 + 4 +
341 4 + 1 + @as(u32, @intCast(username.len)) + 1 +
342 8 + 1 + @as(u32, @intCast(database.len)) + 1 +
343 1;
344 try writer.writeInt(u32, length, .big);
345 try writer.writeAll(&.{ 0, 3, 0, 0 });
346 try writer.writevAll(&.{ "user", "\x00", username, "\x00" });
347 try writer.writevAll(&.{ "database", "\x00", database, "\x00" });
348 try writer.writeAll("\x00");
349 }
350 };
351
352 const SASLInitialResponse = struct {
353 fn writev(writer: anytype, mechanism: []const u8, response_parts: []const []const u8) !void {
354 const parts_len = extras.sumLen(u8, response_parts);
355 const length: u32 = 4 +
356 @as(u32, @intCast(mechanism.len)) + 1 +
357 4 +
358 @as(u32, @intCast(parts_len)) +
359 0;
360 try writer.writeAll("p");
361 try writer.writeInt(u32, length, .big);
362 try writer.writevAll(&.{ mechanism, "\x00" });
363 try writer.writeInt(u32, @intCast(parts_len), .big);
364 try writer.writevAll(response_parts);
365 }
366 };
367
368 const SASLResponse = struct {
369 fn write(writer: anytype, response: []const u8) !void {
370 const length: u32 = 4 +
371 @as(u32, @intCast(response.len)) +
372 0;
373 try writer.writeAll("p");
374 try writer.writeInt(u32, length, .big);
375 try writer.writeAll(response);
376 }
377 };
378};
379
380fn h(Hash: type, str: []const u8) [Hash.digest_length]u8 {
381 var s: Hash = .init(.{});
382 s.update(str);
383 var out: [Hash.digest_length]u8 = @splat(0);
384 s.final(&out);
385 return out;
386}
387
388fn hmac(Hmac: type, key: []const u8, str: []const u8) [Hmac.mac_length]u8 {
389 var s: Hmac = .init(key);
390 s.update(str);
391 var out: [Hmac.mac_length]u8 = @splat(0);
392 s.final(&out);
393 return out;
394}
395fn hmacv(Hmac: type, key: []const u8, strs: []const []const u8) [Hmac.mac_length]u8 {
396 var s: Hmac = .init(key);
397 for (strs) |str| s.update(str);
398 var out: [Hmac.mac_length]u8 = @splat(0);
399 s.final(&out);
400 return out;
401}
402
403fn hi(Hmac: type, str: []const u8, salt: []const u8, i: u32) [Hmac.mac_length]u8 {
404 var dk: [Hmac.mac_length]u8 = @splat(0);
405 std.crypto.pwhash.pbkdf2(&dk, str, salt, i, Hmac) catch unreachable;
406 return dk;
407}
408
409fn xor(l: anytype, r: anytype) @TypeOf(l, r) {
410 var out: @TypeOf(l, r) = @splat(0);
411 for (&l, &r, &out) |a, b, *o| o.* = a ^ b;
412 return out;
413}
414
415fn printError(bufr: *nio.BufferedReader(4096, net.Stream), allocator: std.mem.Allocator) !noreturn {
416 const len = try bufr.readInt(u32, .big);
417 const data = try bufr.readAlloc(allocator, len - 4);
418 defer allocator.free(data);
419 var iter = std.mem.splitScalar(u8, data, 0);
420 while (iter.next()) |f| if (f.len > 0) std.log.err("{c}: {s}", .{ f[0], f[1..] });
421 std.posix.exit(1);
422}
src/sqlite3.zig+2-1
...@@ -9,10 +9,11 @@ db: sqlite.Db = undefined,...@@ -9,10 +9,11 @@ db: sqlite.Db = undefined,
9mutex: std.Thread.Mutex,9mutex: std.Thread.Mutex,
1010
11pub fn connect(allocator: std.mem.Allocator, path: [:0]const u8) !Self {11pub fn connect(allocator: std.mem.Allocator, path: [:0]const u8) !Self {
12 std.log.scoped(.zorm).info("connecting to {s} @ {s}", .{ "sqlite3", path });
12 _ = allocator;13 _ = allocator;
13 return Self{14 return Self{
14 .db = try sqlite.Db.init(.{15 .db = try sqlite.Db.init(.{
15 .mode = sqlite.Db.Mode{ .File = path },16 .mode = .{ .File = path },
16 .open_flags = .{17 .open_flags = .{
17 .write = true,18 .write = true,
18 .create = true,19 .create = true,
zig.mod+5
...@@ -6,3 +6,8 @@ description: The ORM library for Zig....@@ -6,3 +6,8 @@ description: The ORM library for Zig.
6dependencies:6dependencies:
7 - src: git https://github.com/vrischmann/zig-sqlite7 - src: git https://github.com/vrischmann/zig-sqlite
8 - src: git https://github.com/nektro/zig-tracer8 - src: git https://github.com/nektro/zig-tracer
9 - src: git https://github.com/nektro/zig-whatwg-url
10 - src: git https://github.com/nektro/zig-extras
11 - src: git https://github.com/nektro/zig-net
12 - src: git https://github.com/nektro/zig-nio
13 - src: git https://github.com/nektro/zig-sys-linux