mirror of
https://github.com/jetzig-framework/jetquery.git
synced 2026-09-01 23:33:21 -06:00
PostgreSQL adapter
This commit is contained in:
parent
81f7f8799c
commit
26f9dd71c0
14
README.md
14
README.md
@ -1,3 +1,17 @@
|
||||
# JetQuery
|
||||
|
||||
Database query library for [Jetzig](https://github.com/jetzig-framework/jetzig), a web framework written in [Zig](https://ziglang.org/).
|
||||
|
||||
## Testing
|
||||
|
||||
Use the provided _Docker Compose_ configuration to launch a local test database:
|
||||
|
||||
```console
|
||||
docker compose up
|
||||
```
|
||||
|
||||
Run tests:
|
||||
|
||||
```console
|
||||
zig build test
|
||||
```
|
||||
|
||||
134
build.zig
134
build.zig
@ -1,6 +1,6 @@
|
||||
const std = @import("std");
|
||||
|
||||
pub fn build(b: *std.Build) void {
|
||||
pub fn build(b: *std.Build) !void {
|
||||
const target = b.standardTargetOptions(.{});
|
||||
const optimize = b.standardOptimizeOption(.{});
|
||||
|
||||
@ -13,14 +13,146 @@ pub fn build(b: *std.Build) void {
|
||||
|
||||
b.installArtifact(lib);
|
||||
|
||||
const pg_dep = b.dependency("pg", .{ .target = target, .optimize = optimize });
|
||||
const jetcommon_dep = b.dependency("jetcommon", .{ .target = target, .optimize = optimize });
|
||||
const jetcommon_module = jetcommon_dep.module("jetcommon");
|
||||
|
||||
lib.root_module.addImport("pg", pg_dep.module("pg"));
|
||||
lib.root_module.addImport("jetcommon", jetcommon_module);
|
||||
|
||||
const config_path = b.option([]const u8, "jetquery_config_path", "JetQuery configuration file path") orelse "jetquery.config.zig";
|
||||
const config_module = if (try fileExist(config_path))
|
||||
b.createModule(.{ .root_source_file = .{ .cwd_relative = config_path } })
|
||||
else
|
||||
b.createModule(.{ .root_source_file = b.path("src/default_config.zig") });
|
||||
|
||||
const jetquery_module = b.addModule("jetquery", .{ .root_source_file = b.path("src/jetquery.zig") });
|
||||
jetquery_module.addImport("pg", pg_dep.module("pg"));
|
||||
jetquery_module.addImport("jetcommon", jetcommon_module);
|
||||
jetquery_module.addImport("jetquery.config", config_module);
|
||||
|
||||
const migrations_path = b.option([]const u8, "jetquery_migrations_path", "Migrations path") orelse
|
||||
"migrations";
|
||||
const lib_unit_tests = b.addTest(.{
|
||||
.root_source_file = b.path("src/jetquery.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
lib_unit_tests.root_module.addImport("pg", pg_dep.module("pg"));
|
||||
lib_unit_tests.root_module.addImport("jetcommon", jetcommon_module);
|
||||
lib_unit_tests.root_module.addImport("jetquery.config", config_module);
|
||||
|
||||
const run_lib_unit_tests = b.addRunArtifact(lib_unit_tests);
|
||||
|
||||
const test_step = b.step("test", "Run unit tests");
|
||||
test_step.dependOn(&run_lib_unit_tests.step);
|
||||
|
||||
const exe_generate_migrations = b.addExecutable(.{
|
||||
.name = "migrations",
|
||||
.root_source_file = b.path("src/generate_migrations.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
|
||||
const migration_unit_tests = b.addTest(.{
|
||||
.root_source_file = b.path("src/jetquery/Migrate.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
const run_migration_unit_tests = b.addRunArtifact(migration_unit_tests);
|
||||
|
||||
test_step.dependOn(&run_migration_unit_tests.step);
|
||||
|
||||
migration_unit_tests.step.dependOn(&exe_generate_migrations.step);
|
||||
test_step.dependOn(&run_migration_unit_tests.step);
|
||||
|
||||
const run_generate_migrations_cmd = b.addRunArtifact(exe_generate_migrations);
|
||||
const generated_migrations_path = run_generate_migrations_cmd.addOutputFileArg("migrations.zig");
|
||||
|
||||
for (try findMigrations(b.allocator, migrations_path)) |path| {
|
||||
run_generate_migrations_cmd.addFileArg(.{ .cwd_relative = path });
|
||||
}
|
||||
|
||||
const migrations_module = b.createModule(.{ .root_source_file = generated_migrations_path });
|
||||
migrations_module.addImport("jetquery", jetquery_module);
|
||||
migrations_module.addImport("jetquery.config", config_module);
|
||||
migration_unit_tests.root_module.addImport("migrations", migrations_module);
|
||||
migration_unit_tests.root_module.addImport("jetquery", jetquery_module);
|
||||
migration_unit_tests.root_module.addImport("jetcommon", jetcommon_module);
|
||||
|
||||
const jetquery_migrate_module = b.addModule(
|
||||
"jetquery_migrate",
|
||||
.{ .root_source_file = b.path("src/jetquery/Migrate.zig") },
|
||||
);
|
||||
jetquery_migrate_module.addImport("jetquery", jetquery_module);
|
||||
jetquery_migrate_module.addImport("migrations", migrations_module);
|
||||
jetquery_migrate_module.addImport("jetquery.config", config_module);
|
||||
jetquery_migrate_module.addImport("jetcommon", jetcommon_module);
|
||||
|
||||
const jetquery_reflect_module = b.addModule(
|
||||
"jetquery_reflect",
|
||||
.{ .root_source_file = b.path("src/jetquery/reflection/Reflect.zig") },
|
||||
);
|
||||
jetquery_reflect_module.addImport("jetquery", jetquery_module);
|
||||
jetquery_reflect_module.addImport("migrations", migrations_module);
|
||||
jetquery_reflect_module.addImport("jetquery.config", config_module);
|
||||
jetquery_reflect_module.addImport("jetcommon", jetcommon_module);
|
||||
|
||||
const reflect_unit_tests = b.addTest(.{
|
||||
.root_source_file = b.path("src/jetquery/reflection/Reflect.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
const run_reflect_unit_tests = b.addRunArtifact(reflect_unit_tests);
|
||||
reflect_unit_tests.root_module.addImport("jetquery", jetquery_module);
|
||||
reflect_unit_tests.root_module.addImport("jetcommon", jetcommon_module);
|
||||
test_step.dependOn(&run_reflect_unit_tests.step);
|
||||
}
|
||||
|
||||
fn findMigrations(allocator: std.mem.Allocator, path: []const u8) ![][]const u8 {
|
||||
const absolute_path = if (std.fs.path.isAbsolute(path))
|
||||
path
|
||||
else
|
||||
std.fs.cwd().realpathAlloc(allocator, path) catch |err| {
|
||||
switch (err) {
|
||||
error.FileNotFound => return &.{},
|
||||
else => return err,
|
||||
}
|
||||
};
|
||||
|
||||
var dir = std.fs.openDirAbsolute(absolute_path, .{ .iterate = true }) catch |err| {
|
||||
switch (err) {
|
||||
error.FileNotFound => return &.{},
|
||||
else => return err,
|
||||
}
|
||||
};
|
||||
defer dir.close();
|
||||
|
||||
var migrations = std.ArrayList([]const u8).init(allocator);
|
||||
|
||||
var it = dir.iterate();
|
||||
while (try it.next()) |entry| {
|
||||
if (entry.kind != .file) continue;
|
||||
try migrations.append(try std.fs.path.join(allocator, &.{ absolute_path, entry.name }));
|
||||
}
|
||||
|
||||
std.mem.sort([]const u8, migrations.items, {}, cmpString);
|
||||
return try migrations.toOwnedSlice();
|
||||
}
|
||||
|
||||
fn cmpString(_: void, lhs: []const u8, rhs: []const u8) bool {
|
||||
return std.mem.order(u8, lhs, rhs).compare(.lt);
|
||||
}
|
||||
|
||||
fn fileExist(path: []const u8) !bool {
|
||||
const file = std.fs.cwd().openFile(path, .{}) catch |err| {
|
||||
switch (err) {
|
||||
error.FileNotFound => return false,
|
||||
else => return err,
|
||||
}
|
||||
};
|
||||
|
||||
file.close();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -1,72 +1,22 @@
|
||||
.{
|
||||
// This is the default name used by packages depending on this one. For
|
||||
// example, when a user runs `zig fetch --save <url>`, this field is used
|
||||
// as the key in the `dependencies` table. Although the user can choose a
|
||||
// different name, most users will stick with this provided value.
|
||||
//
|
||||
// It is redundant to include "zig" in this name because it is already
|
||||
// within the Zig package namespace.
|
||||
.name = "jetquery",
|
||||
|
||||
// This is a [Semantic Version](https://semver.org/).
|
||||
// In a future version of Zig it will be used for package deduplication.
|
||||
.version = "0.0.0",
|
||||
|
||||
// This field is optional.
|
||||
// This is currently advisory only; Zig does not yet do anything
|
||||
// with this value.
|
||||
//.minimum_zig_version = "0.11.0",
|
||||
|
||||
// This field is optional.
|
||||
// Each dependency must either provide a `url` and `hash`, or a `path`.
|
||||
// `zig build --fetch` can be used to fetch all dependencies of a package, recursively.
|
||||
// Once all dependencies are fetched, `zig build` no longer requires
|
||||
// internet connectivity.
|
||||
.minimum_zig_version = "0.14.0",
|
||||
.dependencies = .{
|
||||
// See `zig fetch --save <url>` for a command-line interface for adding dependencies.
|
||||
//.example = .{
|
||||
// // When updating this field to a new URL, be sure to delete the corresponding
|
||||
// // `hash`, otherwise you are communicating that you expect to find the old hash at
|
||||
// // the new URL.
|
||||
// .url = "https://example.com/foo.tar.gz",
|
||||
//
|
||||
// // This is computed from the file contents of the directory of files that is
|
||||
// // obtained after fetching `url` and applying the inclusion rules given by
|
||||
// // `paths`.
|
||||
// //
|
||||
// // This field is the source of truth; packages do not come from a `url`; they
|
||||
// // come from a `hash`. `url` is just one of many possible mirrors for how to
|
||||
// // obtain a package matching this `hash`.
|
||||
// //
|
||||
// // Uses the [multihash](https://multiformats.io/multihash/) format.
|
||||
// .hash = "...",
|
||||
//
|
||||
// // When this is provided, the package is found in a directory relative to the
|
||||
// // build root. In this case the package's hash is irrelevant and therefore not
|
||||
// // computed. This field and `url` are mutually exclusive.
|
||||
// .path = "foo",
|
||||
|
||||
// // When this is set to `true`, a package is declared to be lazily
|
||||
// // fetched. This makes the dependency only get fetched if it is
|
||||
// // actually used.
|
||||
// .lazy = false,
|
||||
//},
|
||||
.pg = .{
|
||||
.url = "https://github.com/karlseguin/pg.zig/archive/9226a0a256cd000eee2f9652c8c03af8dcbed79b.tar.gz",
|
||||
.hash = "12202b30ebf018ca398f665e51cae6d000fdfe2d08d5ec369e3110f762c548b154a4",
|
||||
},
|
||||
.jetcommon = .{
|
||||
.url = "https://github.com/jetzig-framework/jetcommon/archive/a248776ba56d6cc2b160d593ac3305756adcd26e.tar.gz",
|
||||
.hash = "1220a61e8650f84b28baf31fae5da31712aec4b711b3a41d11ed07c908bac96648d8",
|
||||
},
|
||||
},
|
||||
|
||||
// Specifies the set of files and directories that are included in this package.
|
||||
// Only files and directories listed here are included in the `hash` that
|
||||
// is computed for this package. Only files listed here will remain on disk
|
||||
// when using the zig package manager. As a rule of thumb, one should list
|
||||
// files required for compilation plus any license(s).
|
||||
// Paths are relative to the build root. Use the empty string (`""`) to refer to
|
||||
// the build root itself.
|
||||
// A directory listed here means that all files within, recursively, are included.
|
||||
.paths = .{
|
||||
"build.zig",
|
||||
"build.zig.zon",
|
||||
"src",
|
||||
// For example...
|
||||
//"LICENSE",
|
||||
//"README.md",
|
||||
"LICENSE",
|
||||
"README.md",
|
||||
},
|
||||
}
|
||||
|
||||
8
compose.yml
Normal file
8
compose.yml
Normal file
@ -0,0 +1,8 @@
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16
|
||||
environment:
|
||||
POSTGRES_USER: "postgres"
|
||||
POSTGRES_PASSWORD: "password"
|
||||
ports:
|
||||
- 5432:5432
|
||||
10
jetquery.config.zig
Normal file
10
jetquery.config.zig
Normal file
@ -0,0 +1,10 @@
|
||||
pub const database = .{
|
||||
.testing = .{
|
||||
.adapter = .postgresql,
|
||||
.database = "postgres",
|
||||
.username = "postgres",
|
||||
.hostname = "127.0.0.1",
|
||||
.password = "password",
|
||||
.port = 5432,
|
||||
},
|
||||
};
|
||||
19
migrations/2024-08-26_13-18-52_create_humans.zig
Normal file
19
migrations/2024-08-26_13-18-52_create_humans.zig
Normal file
@ -0,0 +1,19 @@
|
||||
const std = @import("std");
|
||||
const jetquery = @import("jetquery");
|
||||
const t = jetquery.schema.table;
|
||||
|
||||
pub fn up(repo: anytype) !void {
|
||||
try repo.createTable(
|
||||
"humans",
|
||||
&.{
|
||||
t.primaryKey("id", .{}),
|
||||
t.column("name", .string, .{ .not_null = true, .unique = true }),
|
||||
t.timestamps(.{}),
|
||||
},
|
||||
.{},
|
||||
);
|
||||
}
|
||||
|
||||
pub fn down(repo: anytype) !void {
|
||||
try repo.dropTable("humans", .{});
|
||||
}
|
||||
23
migrations/2024-10-26_15-47-48_create_cats.zig
Normal file
23
migrations/2024-10-26_15-47-48_create_cats.zig
Normal file
@ -0,0 +1,23 @@
|
||||
const std = @import("std");
|
||||
const jetquery = @import("jetquery");
|
||||
const t = jetquery.schema.table;
|
||||
|
||||
pub fn up(repo: anytype) !void {
|
||||
try repo.createTable(
|
||||
"cats",
|
||||
&.{
|
||||
t.primaryKey("id", .{}),
|
||||
t.column("name", .string, .{ .not_null = true, .unique = true }),
|
||||
t.column("paws", .integer, .{ .index = true }),
|
||||
t.column("human_id", .integer, .{ .reference = .{ "humans", "id" } }),
|
||||
t.timestamps(.{}),
|
||||
},
|
||||
.{},
|
||||
);
|
||||
|
||||
try repo.createIndex("cats", &.{ "name", "paws" }, .{});
|
||||
}
|
||||
|
||||
pub fn down(repo: anytype) !void {
|
||||
try repo.dropTable("cats", .{});
|
||||
}
|
||||
11
src/default_config.zig
Normal file
11
src/default_config.zig
Normal file
@ -0,0 +1,11 @@
|
||||
pub const database = .{
|
||||
.development = .{
|
||||
.adapter = .null,
|
||||
},
|
||||
.testing = .{
|
||||
.adapter = .null,
|
||||
},
|
||||
.production = .{
|
||||
.adapter = .null,
|
||||
},
|
||||
};
|
||||
61
src/generate_migrations.zig
Normal file
61
src/generate_migrations.zig
Normal file
@ -0,0 +1,61 @@
|
||||
const std = @import("std");
|
||||
|
||||
pub fn main() !void {
|
||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||
defer std.debug.assert(gpa.deinit() == .ok);
|
||||
const gpa_allocator = gpa.allocator();
|
||||
|
||||
var arena = std.heap.ArenaAllocator.init(gpa_allocator);
|
||||
const allocator = arena.allocator();
|
||||
defer arena.deinit();
|
||||
|
||||
const args = try std.process.argsAlloc(allocator);
|
||||
const migrations_module_path = args[1];
|
||||
const migrations: [][]const u8 = if (args.len > 2) args[2..] else &.{};
|
||||
const migrations_file = try std.fs.createFileAbsolute(migrations_module_path, .{});
|
||||
const migrations_module_dir = std.fs.path.dirname(migrations_module_path).?;
|
||||
const writer = migrations_file.writer();
|
||||
try writer.writeAll(
|
||||
\\const jetquery = @import("jetquery");
|
||||
\\pub const Migration = struct {
|
||||
\\ upFn: *const fn(repo: anytype) anyerror!void,
|
||||
\\ downFn: *const fn(repo: anytype) anyerror!void,
|
||||
\\ version: []const u8,
|
||||
\\ name: []const u8,
|
||||
\\};
|
||||
\\pub const migrations = [_]Migration{
|
||||
\\
|
||||
);
|
||||
for (migrations) |migration| {
|
||||
const basename = std.fs.path.basename(migration);
|
||||
const version = basename[0.."2000-01-01_12-00-00".len];
|
||||
try std.fs.copyFileAbsolute(
|
||||
migration,
|
||||
try std.fs.path.join(allocator, &.{ migrations_module_dir, basename }),
|
||||
.{},
|
||||
);
|
||||
try writer.print(
|
||||
\\ .{{
|
||||
\\ .upFn = @import("{0s}").up,
|
||||
\\ .downFn = @import("{0s}").down,
|
||||
\\ .version = "{1s}",
|
||||
\\ .name = "{0s}",
|
||||
\\ }},
|
||||
\\
|
||||
,
|
||||
.{ try zigEscape(allocator, basename), try zigEscape(allocator, version) },
|
||||
);
|
||||
}
|
||||
try writer.writeAll(
|
||||
\\};
|
||||
\\
|
||||
);
|
||||
migrations_file.close();
|
||||
}
|
||||
|
||||
fn zigEscape(allocator: std.mem.Allocator, input: []const u8) ![]const u8 {
|
||||
var buf = std.ArrayList(u8).init(allocator);
|
||||
const writer = buf.writer();
|
||||
try std.zig.stringEscape(input, "", .{}, writer);
|
||||
return try buf.toOwnedSlice();
|
||||
}
|
||||
1497
src/jetquery.zig
1497
src/jetquery.zig
File diff suppressed because it is too large
Load Diff
111
src/jetquery/Connection.zig
Normal file
111
src/jetquery/Connection.zig
Normal file
@ -0,0 +1,111 @@
|
||||
const jetquery = @import("../jetquery.zig");
|
||||
|
||||
pub const Connection = union(enum) {
|
||||
postgresql: jetquery.adapters.PostgresqlAdapter.Connection,
|
||||
|
||||
pub fn execute(
|
||||
self: Connection,
|
||||
query: anytype,
|
||||
caller_info: ?jetquery.debug.CallerInfo,
|
||||
repo: anytype,
|
||||
) !switch (@TypeOf(query).ResultContext) {
|
||||
.one => ?@TypeOf(query).ResultType,
|
||||
.many => jetquery.Result(@TypeOf(repo.*)),
|
||||
.none => void,
|
||||
} {
|
||||
return switch (self) {
|
||||
.postgresql => |*connection| result_blk: {
|
||||
try query.validateValues();
|
||||
try query.validateDelete();
|
||||
var result = try connection.execute(
|
||||
query.sql,
|
||||
query.field_values,
|
||||
caller_info,
|
||||
repo,
|
||||
);
|
||||
break :result_blk switch (@TypeOf(query).ResultContext) {
|
||||
.one => blk: {
|
||||
// TODO: Create a new ResultContext `.unary` instead of hacking it in here.
|
||||
if (query.query_context == .count) {
|
||||
defer result.deinit();
|
||||
const unary = try result.unary(@TypeOf(query).ResultType);
|
||||
try result.drain();
|
||||
return unary;
|
||||
}
|
||||
const row = try result.next(query);
|
||||
defer result.deinit();
|
||||
try result.drain();
|
||||
break :blk row;
|
||||
},
|
||||
.many => result,
|
||||
.none => blk: {
|
||||
try result.drain();
|
||||
defer result.deinit();
|
||||
break :blk {};
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/// Execute SQL with the active adapter without returning a result.
|
||||
pub fn executeVoid(
|
||||
self: Connection,
|
||||
sql: []const u8,
|
||||
values: anytype,
|
||||
caller_info: ?jetquery.debug.CallerInfo,
|
||||
repo: anytype,
|
||||
) !void {
|
||||
var result = switch (self) {
|
||||
inline else => |connection| try connection.execute(sql, values, caller_info, repo),
|
||||
};
|
||||
try result.drain();
|
||||
result.deinit();
|
||||
}
|
||||
|
||||
/// Execute SQL with the active adapter and return a result (same as `execute` but accepts an
|
||||
/// SQL string and values instead of a generated query).
|
||||
pub fn executeSql(
|
||||
self: Connection,
|
||||
sql: []const u8,
|
||||
values: anytype,
|
||||
caller_info: ?jetquery.debug.CallerInfo,
|
||||
repo: anytype,
|
||||
) !jetquery.Result(@TypeOf(repo.*)) {
|
||||
return switch (self) {
|
||||
inline else => |connection| try connection.execute(sql, values, caller_info, repo),
|
||||
};
|
||||
}
|
||||
|
||||
/// Release connection to the pool.
|
||||
pub fn release(self: Connection) void {
|
||||
switch (self) {
|
||||
inline else => |connection| connection.release(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn executeVoidRuntimeBind(
|
||||
self: Connection,
|
||||
sql: []const u8,
|
||||
values: anytype,
|
||||
comptime Args: type,
|
||||
args: Args,
|
||||
field_states: []const jetquery.sql.FieldState,
|
||||
caller_info: ?jetquery.debug.CallerInfo,
|
||||
repo: anytype,
|
||||
) !void {
|
||||
var result = switch (self) {
|
||||
inline else => |connection| try connection.executeRuntimeBind(
|
||||
sql,
|
||||
values,
|
||||
Args,
|
||||
args,
|
||||
field_states,
|
||||
caller_info,
|
||||
repo,
|
||||
),
|
||||
};
|
||||
try result.drain();
|
||||
result.deinit();
|
||||
}
|
||||
};
|
||||
1
src/jetquery/DateTime.zig
Normal file
1
src/jetquery/DateTime.zig
Normal file
@ -0,0 +1 @@
|
||||
const std = @import("std");
|
||||
212
src/jetquery/Migrate.zig
Normal file
212
src/jetquery/Migrate.zig
Normal file
@ -0,0 +1,212 @@
|
||||
const std = @import("std");
|
||||
|
||||
const jetcommon = @import("jetcommon");
|
||||
|
||||
const migrations = @import("migrations").migrations;
|
||||
const Migration = @import("migrations").Migration;
|
||||
const jetquery = @import("jetquery");
|
||||
|
||||
pub const MigrateSchema = struct {
|
||||
pub const Migrations = jetquery.Model(
|
||||
@This(),
|
||||
"jetquery_migrations",
|
||||
struct {
|
||||
version: []const u8,
|
||||
name: []const u8,
|
||||
created_at: jetquery.DateTime,
|
||||
},
|
||||
.{ .primary_key = .version },
|
||||
);
|
||||
};
|
||||
|
||||
pub fn Migrate(adapter_name: jetquery.adapters.Name) type {
|
||||
return struct {
|
||||
const Self = @This();
|
||||
const AdaptedRepo = jetquery.Repo(adapter_name, MigrateSchema);
|
||||
|
||||
repo: *AdaptedRepo,
|
||||
|
||||
pub fn init(repo: *AdaptedRepo) Self {
|
||||
return .{ .repo = repo };
|
||||
}
|
||||
|
||||
/// Run migrations. Create `jetquery_migrations` table if it does not exist. Skip migrations
|
||||
/// already present in `jetquery_migrations`.
|
||||
pub fn migrate(self: Self) !void {
|
||||
try self.createMigrationsTable();
|
||||
|
||||
log("\n* Running migrations.\n", .{});
|
||||
|
||||
var count: usize = 0;
|
||||
|
||||
inline for (migrations) |migration| {
|
||||
if (!try self.isMigrated(migration)) {
|
||||
log("\nExecuting migration: === {s} ===\n", .{migration.name});
|
||||
|
||||
try migration.upFn(self.repo);
|
||||
try self.repo.Query(.Migrations)
|
||||
.insert(.{ .version = migration.version, .name = migration.name })
|
||||
.execute(self.repo);
|
||||
|
||||
count += 1;
|
||||
|
||||
log("\nCompleted migration: === {s} ===\n", .{migration.name});
|
||||
}
|
||||
}
|
||||
|
||||
log("\n* Applied {} migration(s).\n", .{count});
|
||||
}
|
||||
|
||||
pub fn rollback(self: Self) !void {
|
||||
if (migrations.len == 0) {
|
||||
log("No applied migrations detected. Exiting.", .{});
|
||||
return;
|
||||
}
|
||||
|
||||
const last_migration = try self.repo.Query(.Migrations)
|
||||
.orderBy(.{ .version = .desc })
|
||||
.first(self.repo) orelse return;
|
||||
defer self.repo.free(last_migration);
|
||||
|
||||
var applied = false;
|
||||
inline for (migrations) |migration| {
|
||||
if (!applied and std.mem.eql(u8, migration.version, last_migration.version)) {
|
||||
log("\nRolling back migration: === {s} ===\n", .{migration.name});
|
||||
try migration.downFn(self.repo);
|
||||
try self.repo.delete(last_migration);
|
||||
// Just in case we somehow end up with two migrations with the same version (e.g.
|
||||
// user manually copied a file), we want to only apply one of them:
|
||||
applied = true;
|
||||
log("\nCompleted rollback: === {s} ===\n", .{migration.name});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn isMigrated(self: Self, migration: Migration) !bool {
|
||||
const result = try self.repo.Query(.Migrations)
|
||||
.findBy(.{ .version = migration.version }).execute(self.repo);
|
||||
|
||||
return result != null;
|
||||
}
|
||||
|
||||
fn createMigrationsTable(self: Self) !void {
|
||||
try self.repo.createTable(
|
||||
"jetquery_migrations",
|
||||
&.{
|
||||
jetquery.schema.table.primaryKey("version", .{ .type = .string }),
|
||||
jetquery.schema.table.column("name", .string, .{ .not_null = true, .length = 1024 }),
|
||||
jetquery.schema.table.timestamps(.{ .updated_at = false }),
|
||||
},
|
||||
.{ .if_not_exists = true },
|
||||
);
|
||||
}
|
||||
|
||||
fn log(comptime message: []const u8, args: anytype) void {
|
||||
std.debug.print(message ++ "\n", args);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
test "migrate" {
|
||||
try resetDatabase();
|
||||
|
||||
const TestSchema = struct {
|
||||
pub const Cat = jetquery.Model(@This(), "cats", struct {
|
||||
id: i32,
|
||||
name: []const u8,
|
||||
paws: i8,
|
||||
created_at: jetcommon.types.DateTime,
|
||||
updated_at: jetcommon.types.DateTime,
|
||||
human_id: i32,
|
||||
}, .{});
|
||||
pub const Human = jetquery.Model(
|
||||
@This(),
|
||||
"humans",
|
||||
struct {
|
||||
id: i32,
|
||||
name: []const u8,
|
||||
},
|
||||
.{ .relations = .{ .cats = jetquery.relation.hasMany(.Cat, .{}) } },
|
||||
);
|
||||
};
|
||||
|
||||
var migrate_repo = try jetquery.Repo(.postgresql, MigrateSchema).init(
|
||||
std.testing.allocator,
|
||||
.{
|
||||
.adapter = .{
|
||||
.database = "migrate_test",
|
||||
.username = "postgres",
|
||||
.hostname = "127.0.0.1",
|
||||
.password = "password",
|
||||
.port = 5432,
|
||||
},
|
||||
},
|
||||
);
|
||||
defer migrate_repo.deinit();
|
||||
|
||||
const migrate = Migrate(.postgresql).init(&migrate_repo);
|
||||
try migrate.migrate();
|
||||
|
||||
var test_repo = try jetquery.Repo(.postgresql, TestSchema).init(
|
||||
std.testing.allocator,
|
||||
.{
|
||||
.adapter = .{
|
||||
.database = "migrate_test",
|
||||
.username = "postgres",
|
||||
.hostname = "127.0.0.1",
|
||||
.password = "password",
|
||||
.port = 5432,
|
||||
},
|
||||
},
|
||||
);
|
||||
defer test_repo.deinit();
|
||||
|
||||
const migration = try migrate_repo.Query(.Migrations)
|
||||
.findBy(.{ .version = "2024-08-26_13-18-52" })
|
||||
.execute(&migrate_repo);
|
||||
defer migrate_repo.free(migration);
|
||||
|
||||
try std.testing.expect(migration != null);
|
||||
|
||||
const query = test_repo.Query(.Human)
|
||||
.join(.inner, .cats)
|
||||
.select(.{ .id, .name, .{ .cats = .{ .name, .paws, .created_at, .updated_at } } });
|
||||
var result = try test_repo.execute(query);
|
||||
try result.drain();
|
||||
defer result.deinit();
|
||||
|
||||
try migrate.rollback();
|
||||
|
||||
try std.testing.expectError(error.PG, test_repo.execute(query));
|
||||
|
||||
{
|
||||
const humans = try test_repo.Query(.Human).all(&test_repo);
|
||||
defer test_repo.free(humans);
|
||||
}
|
||||
|
||||
try migrate.rollback();
|
||||
|
||||
{
|
||||
const humans = test_repo.Query(.Human).all(&test_repo);
|
||||
try std.testing.expectError(error.PG, humans);
|
||||
}
|
||||
}
|
||||
|
||||
fn resetDatabase() !void {
|
||||
const S = struct {};
|
||||
var repo = try jetquery.Repo(.postgresql, S).init(
|
||||
std.testing.allocator,
|
||||
.{
|
||||
.adapter = .{
|
||||
.database = "postgres",
|
||||
.username = "postgres",
|
||||
.hostname = "127.0.0.1",
|
||||
.password = "password",
|
||||
.port = 5432,
|
||||
},
|
||||
},
|
||||
);
|
||||
defer repo.deinit();
|
||||
try repo.dropDatabase("migrate_test", .{ .if_exists = true });
|
||||
try repo.createDatabase("migrate_test", .{});
|
||||
}
|
||||
626
src/jetquery/Migration.zig
Normal file
626
src/jetquery/Migration.zig
Normal file
@ -0,0 +1,626 @@
|
||||
const std = @import("std");
|
||||
|
||||
const jetcommon = @import("jetcommon");
|
||||
|
||||
allocator: std.mem.Allocator,
|
||||
name: []const u8,
|
||||
options: MigrationOptions,
|
||||
|
||||
const Migration = @This();
|
||||
|
||||
const MigrationOptions = struct {
|
||||
migrations_path: ?[]const u8 = null,
|
||||
command: ?[]const u8 = null,
|
||||
};
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator, name: []const u8, options: MigrationOptions) Migration {
|
||||
return .{ .allocator = allocator, .name = name, .options = options };
|
||||
}
|
||||
|
||||
const Command = struct {
|
||||
command: []const u8,
|
||||
allocator: std.mem.Allocator,
|
||||
|
||||
const Action = enum { create, drop, alter, rename };
|
||||
|
||||
const Modifier = enum {
|
||||
action,
|
||||
name,
|
||||
rename,
|
||||
type,
|
||||
index,
|
||||
unique,
|
||||
reference,
|
||||
};
|
||||
|
||||
pub const DataType = enum {
|
||||
string,
|
||||
integer,
|
||||
float,
|
||||
decimal,
|
||||
boolean,
|
||||
datetime,
|
||||
text,
|
||||
};
|
||||
|
||||
pub const Token = union(enum) {
|
||||
table: Table,
|
||||
column: Column,
|
||||
|
||||
pub const Table = struct {
|
||||
name: ?[]const u8 = null,
|
||||
rename: ?[]const u8 = null,
|
||||
action: ?Action = null,
|
||||
|
||||
pub fn writeUp(self: Table, columns: []const Column, writer: anytype) !void {
|
||||
switch (self.action orelse .create) {
|
||||
.create => try self.writeCreateTable(columns, writer),
|
||||
.drop => try self.writeDropTable(writer),
|
||||
.alter => try self.writeAlterTable(columns, writer),
|
||||
.rename => {}, // Covered by alterTable
|
||||
}
|
||||
}
|
||||
|
||||
pub fn writeDown(self: Table, writer: anytype) !void {
|
||||
if ((self.action orelse .create) == .create) {
|
||||
try writer.print(
|
||||
\\try repo.dropTable("{s}", .{{}});
|
||||
\\
|
||||
,
|
||||
.{self.name orelse return error.MissingTableName},
|
||||
);
|
||||
} else {
|
||||
try writer.writeAll("_ = repo;");
|
||||
}
|
||||
}
|
||||
|
||||
fn writeCreateTable(self: Table, columns: []const Column, writer: anytype) !void {
|
||||
try writer.print(
|
||||
\\try repo.createTable("{s}",
|
||||
, .{
|
||||
self.name orelse return error.MissingTableName,
|
||||
});
|
||||
|
||||
try writer.writeAll("&.{");
|
||||
|
||||
try writer.writeAll(
|
||||
\\t.primaryKey("id", .{}),
|
||||
);
|
||||
for (columns) |column| {
|
||||
try writeColumn(column, writer);
|
||||
}
|
||||
try writer.writeAll("t.timestamps(.{}),");
|
||||
try writer.writeAll("},");
|
||||
try writer.writeAll(".{},);");
|
||||
}
|
||||
|
||||
fn writeDropTable(self: Table, writer: anytype) !void {
|
||||
try writer.print(
|
||||
\\try repo.dropTable("{s}", .{{}});
|
||||
, .{self.name orelse return error.MissingTableName});
|
||||
}
|
||||
|
||||
fn writeAlterTable(self: Table, columns: []const Column, writer: anytype) !void {
|
||||
try writer.print(
|
||||
\\try repo.alterTable("{s}", .{{
|
||||
, .{self.name orelse return error.MissingTableName});
|
||||
|
||||
const has_columns = for (columns) |column| {
|
||||
switch (column.action orelse .create) {
|
||||
.create, .drop, .rename => break true,
|
||||
else => unreachable,
|
||||
}
|
||||
} else false;
|
||||
|
||||
if (has_columns) try writer.writeAll(".columns = .{");
|
||||
|
||||
if (self.rename) |rename_table| {
|
||||
try writer.print(
|
||||
\\.rename = "{s}"{s}
|
||||
, .{ rename_table, if (has_columns) "," else "" });
|
||||
}
|
||||
|
||||
try writeAlterAddColumns(columns, writer);
|
||||
try writeAlterDropColumns(columns, writer);
|
||||
try writeAlterRenameColumns(columns, writer);
|
||||
if (has_columns) try writer.writeAll("},");
|
||||
|
||||
try writer.writeAll("});");
|
||||
}
|
||||
|
||||
fn writeAlterAddColumns(columns: []const Column, writer: anytype) !void {
|
||||
const has_add_column = for (columns) |column| {
|
||||
if (column.action orelse .create == .create) break true;
|
||||
} else false;
|
||||
|
||||
if (has_add_column) try writer.writeAll(".add = &.{");
|
||||
|
||||
for (columns) |column| {
|
||||
if (column.action orelse .create == .create) {
|
||||
try writeColumn(column, writer);
|
||||
}
|
||||
}
|
||||
|
||||
if (has_add_column) try writer.writeAll("},");
|
||||
}
|
||||
|
||||
fn writeAlterDropColumns(columns: []const Column, writer: anytype) !void {
|
||||
const has_drop_column = for (columns) |column| {
|
||||
if (column.action orelse .create == .drop) break true;
|
||||
} else false;
|
||||
|
||||
if (has_drop_column) try writer.writeAll(".drop = &.{");
|
||||
|
||||
for (columns) |column| {
|
||||
if (column.action orelse .create == .drop) {
|
||||
try writer.print(
|
||||
\\"{s},"
|
||||
,
|
||||
.{column.name orelse return error.MissingColumnName},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (has_drop_column) try writer.writeAll("},");
|
||||
}
|
||||
|
||||
fn writeAlterRenameColumns(columns: []const Column, writer: anytype) !void {
|
||||
var count: usize = 0;
|
||||
for (columns) |column| {
|
||||
if (column.action orelse .create == .rename) {
|
||||
try writer.print(
|
||||
\\.rename = .{{ .from = "{s}", .to = "{s}" }},
|
||||
,
|
||||
.{
|
||||
column.name orelse return error.MissingColumnName,
|
||||
column.rename orelse return error.MissingColumnName,
|
||||
},
|
||||
);
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (count > 1) {
|
||||
std.log.err(
|
||||
"Multiple column renames are not permitted. " ++
|
||||
"Create separate migrations to rename multiple columns.",
|
||||
.{},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn writeColumn(column: Column, writer: anytype) !void {
|
||||
try writer.print(
|
||||
\\t.column("{s}", .{s}, .{{
|
||||
, .{
|
||||
column.name orelse return error.MissingColumnName,
|
||||
@tagName(column.type orelse .string),
|
||||
});
|
||||
var options_count: usize = 0;
|
||||
inline for (comptime std.enums.values(Column.options)) |tag| {
|
||||
if (@field(column, @tagName(tag))) |option| {
|
||||
if (option) options_count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (column.reference_column) |_| options_count += 1;
|
||||
|
||||
var index: usize = 0;
|
||||
inline for (comptime std.enums.values(Column.options)) |field| {
|
||||
if (@field(column, @tagName(field))) |option| {
|
||||
if (option) {
|
||||
const sep = if (index + 1 < options_count) "," else "";
|
||||
try writer.print(".{s} = true{s}", .{ @tagName(field), sep });
|
||||
index += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (try column.referenceInfo()) |reference_info| {
|
||||
try writer.print(
|
||||
\\.reference = .{{"{s}", "{s}"}}
|
||||
, .{ reference_info[0], reference_info[1] });
|
||||
}
|
||||
|
||||
try writer.writeAll("}),");
|
||||
}
|
||||
};
|
||||
|
||||
pub const Column = struct {
|
||||
name: ?[]const u8 = null,
|
||||
rename: ?[]const u8 = null,
|
||||
action: ?Action = null,
|
||||
type: ?DataType = null,
|
||||
index: ?bool = null,
|
||||
unique: ?bool = null,
|
||||
not_null: ?bool = null,
|
||||
reference: ?bool = null,
|
||||
reference_column: ?[]const u8 = null,
|
||||
|
||||
pub const options = enum { unique, not_null, index };
|
||||
|
||||
pub fn referenceInfo(self: Column) !?[2][]const u8 {
|
||||
if (self.reference_column == null) return null;
|
||||
|
||||
var info: [2][]const u8 = undefined;
|
||||
var it = std.mem.tokenizeScalar(u8, self.reference_column.?, '.');
|
||||
var index: usize = 0;
|
||||
while (it.next()) |identifier| : (index += 1) {
|
||||
std.debug.assert(index < 2);
|
||||
info[index] = identifier;
|
||||
}
|
||||
std.debug.assert(index == 2);
|
||||
return info;
|
||||
}
|
||||
};
|
||||
|
||||
pub fn hasName(self: Token) bool {
|
||||
return switch (self) {
|
||||
inline else => |token| blk: {
|
||||
if (@hasField(@TypeOf(token), "name")) {
|
||||
break :blk token.name != null;
|
||||
} else {
|
||||
// If we don't have a name field, return true to allow other attributes
|
||||
// to be assigned (name always takes precedence as we expect it as the
|
||||
// first token after the object designator, e.g.
|
||||
// `column:foobar:index:unique` - `foobar` is the name.
|
||||
break :blk true;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
pub fn hasAction(self: Token) bool {
|
||||
return switch (self) {
|
||||
inline else => |token| token.action != null,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn isRename(self: Token) bool {
|
||||
return switch (self) {
|
||||
inline else => |token| token.action orelse .create == .rename,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn hasRename(self: Token) bool {
|
||||
return switch (self) {
|
||||
inline else => |token| token.rename != null,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn set(self: *Token, modifier: Modifier, value: anytype) void {
|
||||
switch (self.*) {
|
||||
inline else => |*token| {
|
||||
const T = @TypeOf(token.*);
|
||||
inline for (std.meta.fields(T)) |field| {
|
||||
if (comptime hasField(T, field.name, @TypeOf(value))) {
|
||||
if (std.mem.eql(u8, field.name, @tagName(modifier))) {
|
||||
@field(token, field.name) = @field(token, field.name) orelse value;
|
||||
}
|
||||
} else if (comptime hasEnum(T, field.name, @TypeOf(value))) {
|
||||
inline for (comptime std.enums.values(FieldType(T, field.name))) |tag| {
|
||||
if (std.mem.eql(u8, @tagName(tag), value)) {
|
||||
@field(token, field.name) = tag;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
fn isAction(modifier: []const u8) bool {
|
||||
for (std.enums.values(Action)) |action| {
|
||||
if (std.mem.eql(u8, @tagName(action), modifier)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const TokenIterator = struct {
|
||||
arg_iterator: *std.mem.TokenIterator(u8, .any),
|
||||
|
||||
pub fn next(self: TokenIterator) !?Token {
|
||||
while (self.arg_iterator.next()) |arg| {
|
||||
var modifiers_it = std.mem.tokenizeScalar(u8, arg, ':');
|
||||
var maybe_token: ?Token = null;
|
||||
while (modifiers_it.next()) |modifier| {
|
||||
if (maybe_token) |*token| {
|
||||
if (!token.hasAction() and isAction(modifier)) {
|
||||
token.set(.action, modifier);
|
||||
} else if (!token.hasName()) {
|
||||
token.set(.name, modifier);
|
||||
} else if (modifierToken(modifier)) |modifier_token| {
|
||||
token.set(modifier_token, true);
|
||||
} else if (isType(modifier)) {
|
||||
token.set(.type, modifier);
|
||||
} else if (token.isRename() and !token.hasRename()) {
|
||||
token.set(.rename, modifier);
|
||||
} else if (token.* == .column and token.*.column.reference == true) {
|
||||
token.column.reference_column = modifier;
|
||||
} else {
|
||||
return error.InvalidMigrationCommand;
|
||||
}
|
||||
} else {
|
||||
maybe_token = if (std.mem.eql(u8, modifier, "table"))
|
||||
Token{ .table = .{} }
|
||||
else if (std.mem.eql(u8, modifier, "column"))
|
||||
Token{ .column = .{} }
|
||||
else if (std.mem.eql(u8, modifier, "rename"))
|
||||
Token{ .table = .{ .action = .alter, .rename = modifier } }
|
||||
else {
|
||||
return error.InvalidMigrationCommand;
|
||||
};
|
||||
}
|
||||
}
|
||||
return maybe_token;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
fn modifierToken(modifier: []const u8) ?Modifier {
|
||||
inline for (comptime std.enums.values(Modifier)) |value| {
|
||||
if (std.mem.eql(u8, @tagName(value), modifier)) return value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
pub fn write(self: Command, writer: anytype) !void {
|
||||
var arg_iterator = std.mem.tokenizeAny(u8, self.command, &std.ascii.whitespace);
|
||||
var token_iterator = TokenIterator{ .arg_iterator = &arg_iterator };
|
||||
|
||||
var up_buf = std.ArrayList(u8).init(self.allocator);
|
||||
const up_writer = up_buf.writer();
|
||||
|
||||
var down_buf = std.ArrayList(u8).init(self.allocator);
|
||||
const down_writer = down_buf.writer();
|
||||
|
||||
var columns = std.ArrayList(Command.Token.Column).init(self.allocator);
|
||||
var maybe_table: ?Command.Token.Table = null;
|
||||
|
||||
while (try token_iterator.next()) |token| {
|
||||
switch (token) {
|
||||
.table => |table| {
|
||||
maybe_table = table;
|
||||
},
|
||||
.column => |column| {
|
||||
try columns.append(column);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if (maybe_table) |table| {
|
||||
try table.writeUp(columns.items, up_writer);
|
||||
try table.writeDown(down_writer);
|
||||
}
|
||||
try writer.print(migration_template, .{ up_buf.items, down_buf.items });
|
||||
}
|
||||
};
|
||||
|
||||
const migration_template =
|
||||
\\const std = @import("std");
|
||||
\\const jetquery = @import("jetquery");
|
||||
\\const t = jetquery.schema.table;
|
||||
\\
|
||||
\\pub fn up(repo: anytype) !void {{
|
||||
\\{s}
|
||||
\\}}
|
||||
\\
|
||||
\\pub fn down(repo: anytype) !void {{
|
||||
\\{s}
|
||||
\\}}
|
||||
\\
|
||||
;
|
||||
const default_migration = std.fmt.comptimePrint(migration_template, .{
|
||||
\\ try repo.createTable(
|
||||
\\ "my_table",
|
||||
\\ &.{
|
||||
\\ t.primaryKey("id", .{{}}),
|
||||
\\ t.column("my_string", .string, .{{}}),
|
||||
\\ t.column("my_integer", .integer, .{{}}),
|
||||
\\ t.timestamps(.{{}}),
|
||||
\\ },
|
||||
\\ .{{}},
|
||||
\\ );
|
||||
,
|
||||
\\ try repo.dropTable("my_table", .{{}});
|
||||
});
|
||||
|
||||
pub fn save(self: Migration) ![]const u8 {
|
||||
const content = try self.render();
|
||||
|
||||
var dir = if (self.options.migrations_path) |path|
|
||||
try std.fs.openDirAbsolute(path, .{})
|
||||
else
|
||||
try std.fs.cwd().openDir("migrations", .{});
|
||||
defer dir.close();
|
||||
|
||||
var timestamp_buf: [19]u8 = undefined;
|
||||
const prefix = try timestamp(×tamp_buf);
|
||||
const filename = try std.mem.concat(self.allocator, u8, &.{ prefix, "_", self.name, ".zig" });
|
||||
const migration_file = try dir.createFile(filename, .{ .exclusive = true });
|
||||
defer migration_file.close();
|
||||
|
||||
const writer = migration_file.writer();
|
||||
try writer.writeAll(content);
|
||||
const realpath = try dir.realpathAlloc(self.allocator, filename);
|
||||
|
||||
return realpath;
|
||||
}
|
||||
|
||||
pub fn render(self: Migration) ![]const u8 {
|
||||
var arena = std.heap.ArenaAllocator.init(self.allocator);
|
||||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
|
||||
var buf = std.ArrayList(u8).init(alloc);
|
||||
const writer = buf.writer();
|
||||
|
||||
if (self.options.command) |cmd| {
|
||||
const command = Command{ .allocator = alloc, .command = cmd };
|
||||
try command.write(writer);
|
||||
} else {
|
||||
try writer.writeAll(default_migration);
|
||||
}
|
||||
return try jetcommon.fmt.zig(
|
||||
self.allocator,
|
||||
buf.items,
|
||||
"Found errors in generated migration.",
|
||||
);
|
||||
}
|
||||
|
||||
fn timestamp(buf: []u8) ![]const u8 {
|
||||
const datetime = jetcommon.types.DateTime.now();
|
||||
const date = datetime.date();
|
||||
const time = datetime.time();
|
||||
var stream = std.io.fixedBufferStream(buf);
|
||||
const writer = stream.writer();
|
||||
try writer.print(
|
||||
"{d:04}-{d:02}-{d:02}_{d:02}-{d:02}-{d:02}",
|
||||
// TODO: Fix jetcommon types to expose these directly
|
||||
.{ @as(u16, @intCast(date.zul_date.year)), date.zul_date.month, date.zul_date.day, time.zul_time.hour, time.zul_time.min, time.zul_time.sec },
|
||||
);
|
||||
return stream.getWritten();
|
||||
}
|
||||
|
||||
inline fn FieldType(T: type, comptime name: []const u8) type {
|
||||
const tag = std.enums.nameCast(std.meta.FieldEnum(T), name);
|
||||
const F = std.meta.fieldInfo(T, tag);
|
||||
return switch (@typeInfo(F.type)) {
|
||||
.optional => |info| info.child,
|
||||
else => F,
|
||||
};
|
||||
}
|
||||
|
||||
inline fn hasField(T: type, comptime name: []const u8, VT: type) bool {
|
||||
return @hasField(T, name) and FieldType(T, name) == VT;
|
||||
}
|
||||
|
||||
inline fn hasEnum(T: type, comptime name: []const u8, VT: type) bool {
|
||||
if (VT != []const u8) return false;
|
||||
if (!@hasField(T, name)) return false;
|
||||
|
||||
const FT = FieldType(T, name);
|
||||
|
||||
return @typeInfo(FT) == .@"enum";
|
||||
}
|
||||
|
||||
inline fn isType(name: []const u8) bool {
|
||||
inline for (comptime std.enums.values(Command.DataType)) |tag| {
|
||||
if (std.mem.eql(u8, name, @tagName(tag))) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
test "default migration" {
|
||||
const migration = Migration.init(std.testing.allocator, "test_migration", .{});
|
||||
|
||||
const rendered = try migration.render();
|
||||
defer std.testing.allocator.free(rendered);
|
||||
|
||||
try std.testing.expectEqualStrings(default_migration, rendered);
|
||||
}
|
||||
|
||||
test "migration from command line: create table" {
|
||||
const command = "table:create:cats column:name:string:index:unique column:paws:integer column:human_id:index:reference:humans.id";
|
||||
|
||||
const migration = Migration.init(
|
||||
std.testing.allocator,
|
||||
"test_migration",
|
||||
.{ .command = command },
|
||||
);
|
||||
const rendered = try migration.render();
|
||||
defer std.testing.allocator.free(rendered);
|
||||
|
||||
try std.testing.expectEqualStrings(
|
||||
\\const std = @import("std");
|
||||
\\const jetquery = @import("jetquery");
|
||||
\\const t = jetquery.schema.table;
|
||||
\\
|
||||
\\pub fn up(repo: anytype) !void {
|
||||
\\ try repo.createTable(
|
||||
\\ "cats",
|
||||
\\ &.{
|
||||
\\ t.primaryKey("id", .{}),
|
||||
\\ t.column("name", .string, .{ .unique = true, .index = true }),
|
||||
\\ t.column("paws", .integer, .{}),
|
||||
\\ t.column("human_id", .string, .{ .index = true, .reference = .{ "humans", "id" } }),
|
||||
\\ t.timestamps(.{}),
|
||||
\\ },
|
||||
\\ .{},
|
||||
\\ );
|
||||
\\}
|
||||
\\
|
||||
\\pub fn down(repo: anytype) !void {
|
||||
\\ try repo.dropTable("cats", .{});
|
||||
\\}
|
||||
\\
|
||||
, rendered);
|
||||
}
|
||||
|
||||
test "migration from command line: drop table" {
|
||||
const command = "table:drop:cats";
|
||||
|
||||
const migration = Migration.init(
|
||||
std.testing.allocator,
|
||||
"test_migration",
|
||||
.{ .command = command },
|
||||
);
|
||||
const rendered = try migration.render();
|
||||
defer std.testing.allocator.free(rendered);
|
||||
|
||||
try std.testing.expectEqualStrings(
|
||||
\\const std = @import("std");
|
||||
\\const jetquery = @import("jetquery");
|
||||
\\const t = jetquery.schema.table;
|
||||
\\
|
||||
\\pub fn up(repo: anytype) !void {
|
||||
\\ try repo.dropTable("cats", .{});
|
||||
\\}
|
||||
\\
|
||||
\\pub fn down(repo: anytype) !void {
|
||||
\\ _ = repo;
|
||||
\\}
|
||||
\\
|
||||
, rendered);
|
||||
}
|
||||
|
||||
test "migration from command line: alter table" {
|
||||
// XXX: This is an incoherent migration (renaming table while adding columns not permitted)
|
||||
// but it tests a lot of variations all in one command. We let the database fail if the
|
||||
// migration is not coherent.
|
||||
const command = "table:alter:cats column:color:string:index:unique column:rename:paws:feet column:drop:name rename:dogs";
|
||||
|
||||
const migration = Migration.init(
|
||||
std.testing.allocator,
|
||||
"test_migration",
|
||||
.{ .command = command },
|
||||
);
|
||||
const rendered = try migration.render();
|
||||
defer std.testing.allocator.free(rendered);
|
||||
|
||||
try std.testing.expectEqualStrings(
|
||||
\\const std = @import("std");
|
||||
\\const jetquery = @import("jetquery");
|
||||
\\const t = jetquery.schema.table;
|
||||
\\
|
||||
\\pub fn up(repo: anytype) !void {
|
||||
\\ try repo.alterTable("dogs", .{
|
||||
\\ .columns = .{
|
||||
\\ .rename = "rename",
|
||||
\\ .add = &.{
|
||||
\\ t.column("color", .string, .{ .unique = true, .index = true }),
|
||||
\\ },
|
||||
\\ .drop = &.{"name,"},
|
||||
\\ .rename = .{ .from = "paws", .to = "feet" },
|
||||
\\ },
|
||||
\\ });
|
||||
\\}
|
||||
\\
|
||||
\\pub fn down(repo: anytype) !void {
|
||||
\\ _ = repo;
|
||||
\\}
|
||||
\\
|
||||
, rendered);
|
||||
}
|
||||
133
src/jetquery/Model.zig
Normal file
133
src/jetquery/Model.zig
Normal file
@ -0,0 +1,133 @@
|
||||
const std = @import("std");
|
||||
|
||||
const jetquery = @import("../jetquery.zig");
|
||||
|
||||
/// Abstraction of a database table. Define a schema with:
|
||||
/// ```zig
|
||||
/// const Schema = struct {
|
||||
/// pub const Cat = Table("cats", struct { name: []const u8, paws: usize }, .{});
|
||||
/// };
|
||||
/// ```
|
||||
pub fn Model(Schema: type, comptime table_name: []const u8, T: type, options: anytype) type {
|
||||
return struct {
|
||||
// TODO: Implement `format()`
|
||||
|
||||
pub const Definition = T;
|
||||
const Self = @This();
|
||||
pub const name = table_name;
|
||||
pub const info = .{ .schema = Schema };
|
||||
|
||||
pub const relations = if (@hasField(
|
||||
@TypeOf(options),
|
||||
"relations",
|
||||
)) options.relations else .{};
|
||||
|
||||
pub const primary_key = if (@hasField(
|
||||
@TypeOf(options),
|
||||
"primary_key",
|
||||
)) jetquery.util.stringMaybeEnum(options.primary_key) else "id";
|
||||
|
||||
pub fn init(args: anytype) RecordType(@TypeOf(args)) {
|
||||
var record: RecordType(@TypeOf(args)) = undefined;
|
||||
|
||||
record.__jetquery = .{ .args = args };
|
||||
|
||||
inline for (std.meta.fields(@TypeOf(args))) |field| {
|
||||
@field(record, field.name) = @field(args, field.name);
|
||||
}
|
||||
return record;
|
||||
}
|
||||
|
||||
pub fn Relation(comptime relation_name: []const u8) type {
|
||||
comptime {
|
||||
for (relations) |relation| {
|
||||
if (std.mem.eql(u8, relation.relation_name, relation_name)) {
|
||||
return relation;
|
||||
}
|
||||
}
|
||||
@compileError(std.fmt.comptimePrint(
|
||||
"Failed matching relation `{s}` on `{s}`",
|
||||
.{ relation_name, name },
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn columns() [std.meta.fields(Definition).len]jetquery.columns.Column {
|
||||
comptime {
|
||||
const fields = std.meta.fields(Definition);
|
||||
var buf: [fields.len]jetquery.columns.Column = undefined;
|
||||
for (fields, 0..) |field, index| {
|
||||
buf[index] = .{
|
||||
.name = field.name,
|
||||
.table = @This(),
|
||||
.type = field.type,
|
||||
};
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn column(comptime column_name: []const u8) jetquery.columns.Column {
|
||||
comptime {
|
||||
return for (columns()) |col| {
|
||||
if (std.mem.eql(u8, column_name, col.name)) break col;
|
||||
} else @compileError(std.fmt.comptimePrint(
|
||||
"No column named `{s}` defined in Schema for `{s}'",
|
||||
.{ column_name, table_name },
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
fn RecordType(Args: type) type {
|
||||
comptime {
|
||||
const fields = std.meta.fields(T);
|
||||
var struct_fields: [fields.len + 3]std.builtin.Type.StructField = undefined;
|
||||
|
||||
for (fields, 0..) |field, index| {
|
||||
struct_fields[index] = jetquery.fields.structField(field.name, field.type);
|
||||
}
|
||||
|
||||
struct_fields[fields.len] = jetquery.fields.structFieldComptime(
|
||||
"__jetquery_model",
|
||||
@This(),
|
||||
);
|
||||
|
||||
struct_fields[fields.len + 1] = jetquery.fields.structFieldComptime(
|
||||
"__jetquery_schema",
|
||||
Schema,
|
||||
);
|
||||
|
||||
const args_field = jetquery.fields.structField("args", Args);
|
||||
const JetQuery = jetquery.fields.structType(&.{args_field});
|
||||
|
||||
struct_fields[fields.len + 2] = jetquery.fields.structField(
|
||||
"__jetquery",
|
||||
JetQuery,
|
||||
);
|
||||
|
||||
return jetquery.fields.structType(&struct_fields);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn defaultForeignKey() []const u8 {
|
||||
comptime {
|
||||
for (@typeInfo(Schema).@"struct".decls) |decl| {
|
||||
const table = @field(Schema, decl.name);
|
||||
|
||||
if (std.mem.eql(u8, table.name, @This().name)) {
|
||||
var buf: [decl.name.len]u8 = undefined;
|
||||
return std.ascii.lowerString(&buf, decl.name) ++ "_id";
|
||||
}
|
||||
}
|
||||
|
||||
@compileError("Failed matching `" ++ @typeName(@This()) ++ "` in schema.");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn defaultOrderBy() []const jetquery.sql.OrderClause {
|
||||
if (!@hasField(Definition, primary_key)) return &.{};
|
||||
|
||||
return &.{.{ .column = column(primary_key), .direction = .ascending }};
|
||||
}
|
||||
};
|
||||
}
|
||||
1400
src/jetquery/Query.zig
Normal file
1400
src/jetquery/Query.zig
Normal file
File diff suppressed because it is too large
Load Diff
1382
src/jetquery/Repo.zig
Normal file
1382
src/jetquery/Repo.zig
Normal file
File diff suppressed because it is too large
Load Diff
365
src/jetquery/Result.zig
Normal file
365
src/jetquery/Result.zig
Normal file
@ -0,0 +1,365 @@
|
||||
const std = @import("std");
|
||||
|
||||
const jetquery = @import("../jetquery.zig");
|
||||
const AuxiliaryQuery = @import("Query.zig").AuxiliaryQuery;
|
||||
|
||||
/// A result of an executed query.
|
||||
pub fn Result(AdaptedRepo: type) type {
|
||||
return union(enum) {
|
||||
postgresql: jetquery.adapters.PostgresqlAdapter.Result(AdaptedRepo),
|
||||
|
||||
const Self = @This();
|
||||
|
||||
pub fn deinit(self: *Self) void {
|
||||
switch (self.*) {
|
||||
inline else => |*adapted_result| adapted_result.deinit(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn drain(self: *Self) !void {
|
||||
switch (self.*) {
|
||||
inline else => |*adapted_result| try adapted_result.drain(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next(self: *Self, query: anytype) !?@TypeOf(query).ResultType {
|
||||
const ResultType = @TypeOf(query).ResultType;
|
||||
|
||||
return switch (self.*) {
|
||||
inline else => |*adapted_result| blk: {
|
||||
var row = try adapted_result.next(query) orelse break :blk null;
|
||||
errdefer adapted_result.deinit();
|
||||
errdefer adapted_result.repo._freeRow(row);
|
||||
|
||||
extendInternalFields(@TypeOf(query), &row);
|
||||
|
||||
const primary_key = @TypeOf(query).info.Model.primary_key;
|
||||
const primary_key_present = @hasField(
|
||||
@TypeOf(query).info.Model.Definition,
|
||||
primary_key,
|
||||
);
|
||||
|
||||
// Create a secondary connection for fetching relations if needed. This allows us
|
||||
// to continue iterating over the primary query and fetching relations on each
|
||||
// iteration. Since these connections are backed by a pool (in pg.zig) we should
|
||||
// be okay acquiring a new connection for each call to `next()`.
|
||||
var connection = if (query.auxiliary_queries.len > 0)
|
||||
try adapted_result.repo.connect()
|
||||
else {};
|
||||
defer if (query.auxiliary_queries.len > 0) connection.release();
|
||||
|
||||
inline for (query.auxiliary_queries) |aux_query| {
|
||||
const foreign_key = comptime aux_query.relation.foreign_key orelse
|
||||
@TypeOf(query).info.Model.defaultForeignKey();
|
||||
|
||||
const Args = WhereArgs(aux_query, foreign_key, .one);
|
||||
var args: Args = undefined;
|
||||
|
||||
const q = if (comptime primary_key_present) q_blk: {
|
||||
@field(args, foreign_key) = @field(row, primary_key);
|
||||
break :q_blk aux_query.baseQuery().where(args);
|
||||
} else @compileError(std.fmt.comptimePrint(
|
||||
"Unable to fetch relation records for `{s}` without primary key.",
|
||||
.{aux_query.relation.relation_name},
|
||||
));
|
||||
|
||||
var aux_result = try connection.execute(
|
||||
q,
|
||||
adapted_result.caller_info,
|
||||
adapted_result.repo,
|
||||
);
|
||||
defer aux_result.deinit();
|
||||
|
||||
const aux_type = AuxType(ResultType, aux_query.relation);
|
||||
|
||||
var aux_rows = std.ArrayList(aux_type).init(adapted_result.allocator);
|
||||
while (try aux_result.next(q)) |aux_row| {
|
||||
try aux_rows.append(mergeAux(
|
||||
aux_type,
|
||||
q,
|
||||
@TypeOf(aux_row),
|
||||
aux_row,
|
||||
));
|
||||
}
|
||||
@field(row, aux_query.relation.relation_name) = try aux_rows.toOwnedSlice();
|
||||
}
|
||||
break :blk row;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
pub fn all(self: *Self, query: anytype) ![]@TypeOf(query).ResultType {
|
||||
const ResultType = @TypeOf(query).ResultType;
|
||||
|
||||
return switch (self.*) {
|
||||
inline else => |*adapted_result| blk: {
|
||||
var rows = try adapted_result.all(query);
|
||||
const MergedRow = MergedRowType(query.auxiliary_queries, ResultType);
|
||||
const primary_key = @TypeOf(query).info.Model.primary_key;
|
||||
const primary_key_present = @hasField(
|
||||
@TypeOf(query).info.Model.Definition,
|
||||
primary_key,
|
||||
);
|
||||
|
||||
var map = Map(@TypeOf(query), MergedRow, primary_key)
|
||||
.init(adapted_result.allocator);
|
||||
defer map.deinit();
|
||||
|
||||
for (rows, 0..) |row, index| {
|
||||
if (comptime primary_key_present) {
|
||||
const id = @field(row, primary_key);
|
||||
try map.id_map.put(id, index);
|
||||
try map.id_array.append(id);
|
||||
}
|
||||
var adapted_row = row;
|
||||
extendInternalFields(@TypeOf(query), &adapted_row);
|
||||
|
||||
var merged_row: MergedRow = undefined;
|
||||
inline for (query.auxiliary_queries) |init_aux_query| {
|
||||
const aux_type = AuxType(ResultType, init_aux_query.relation);
|
||||
@field(
|
||||
merged_row,
|
||||
init_aux_query.relation.relation_name,
|
||||
) = std.ArrayList(aux_type).init(adapted_result.allocator);
|
||||
}
|
||||
const aux_values = try map.aux_map.getOrPut(index);
|
||||
aux_values.value_ptr.* = merged_row;
|
||||
rows[index] = adapted_row;
|
||||
}
|
||||
|
||||
// Execute secondary queries (hasMany relations when `include` is used) where
|
||||
// foreign keys match the primary keys returned by the primary query, then merge
|
||||
// the results together.
|
||||
inline for (query.auxiliary_queries) |aux_query| {
|
||||
const foreign_key = comptime aux_query.relation.foreign_key orelse
|
||||
@TypeOf(query).info.Model.defaultForeignKey();
|
||||
|
||||
const Args = WhereArgs(aux_query, foreign_key, .many);
|
||||
var args: Args = undefined;
|
||||
|
||||
const q = if (comptime primary_key_present) q_blk: {
|
||||
@field(args, foreign_key) = map.id_array.items;
|
||||
break :q_blk aux_query.baseQuery().where(args);
|
||||
} else @compileError(std.fmt.comptimePrint(
|
||||
"Unable to fetch relation records for `{s}` without primary key.",
|
||||
.{aux_query.relation.relation_name},
|
||||
));
|
||||
|
||||
var aux_result = try adapted_result.repo.executeInternal(
|
||||
q,
|
||||
adapted_result.caller_info,
|
||||
);
|
||||
defer aux_result.deinit();
|
||||
|
||||
const aux_type = AuxType(ResultType, aux_query.relation);
|
||||
|
||||
while (try aux_result.next(q)) |aux_row| {
|
||||
const adapted_aux_row = mergeAux(
|
||||
aux_type,
|
||||
q,
|
||||
@TypeOf(aux_row),
|
||||
aux_row,
|
||||
);
|
||||
|
||||
if (comptime primary_key_present) try mapAux(
|
||||
aux_query,
|
||||
aux_type,
|
||||
adapted_aux_row,
|
||||
@TypeOf(aux_row),
|
||||
aux_row,
|
||||
foreign_key,
|
||||
@TypeOf(map),
|
||||
&map,
|
||||
);
|
||||
}
|
||||
try aux_result.drain();
|
||||
}
|
||||
|
||||
var it = map.aux_map.iterator();
|
||||
while (it.next()) |entry| {
|
||||
inline for (std.meta.fields(@TypeOf(entry.value_ptr.*))) |field| {
|
||||
@field(rows[entry.key_ptr.*], field.name) = try @field(
|
||||
entry.value_ptr.*,
|
||||
field.name,
|
||||
).toOwnedSlice();
|
||||
}
|
||||
}
|
||||
|
||||
break :blk rows;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
pub fn unary(self: *Self, T: type) !T {
|
||||
return switch (self.*) {
|
||||
inline else => |*adapted_result| try adapted_result.unary(T),
|
||||
};
|
||||
}
|
||||
|
||||
fn extendInternalFields(Query: type, result: *Query.ResultType) void {
|
||||
result.__jetquery_model = Query.info.Model;
|
||||
result.__jetquery_schema = Query.info.Schema;
|
||||
|
||||
const originals = std.meta.fields(@TypeOf(result.__jetquery.original_values));
|
||||
|
||||
inline for (originals) |field| {
|
||||
@field(result.__jetquery.original_values, field.name) = @field(result, field.name);
|
||||
}
|
||||
|
||||
inline for (Query.relations) |relation| {
|
||||
if (comptime relation.relation_type != .belongs_to) continue;
|
||||
|
||||
inline for (relation.select_columns) |select_column| {
|
||||
const relation_field = @field(result, relation.relation_name);
|
||||
const value = @field(relation_field, select_column.name);
|
||||
@field(
|
||||
@field(result, relation.relation_name).__jetquery.original_values,
|
||||
select_column.name,
|
||||
) = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub inline fn duration(self: Result) i64 {
|
||||
return switch (self) {
|
||||
inline else => |adapted_result| adapted_result.duration,
|
||||
};
|
||||
}
|
||||
|
||||
fn mergeAux(aux_type: type, q: anytype, T: type, aux_row: T) aux_type {
|
||||
var extended_aux_row = aux_row;
|
||||
extendInternalFields(@TypeOf(q), &extended_aux_row);
|
||||
|
||||
var adapted_aux_row: aux_type = undefined;
|
||||
|
||||
inline for (std.meta.fields(aux_type)) |field| {
|
||||
if (comptime std.mem.startsWith(u8, field.name, "__jetquery")) continue;
|
||||
@field(adapted_aux_row, field.name) = @field(aux_row, field.name);
|
||||
@field(
|
||||
adapted_aux_row.__jetquery.original_values,
|
||||
field.name,
|
||||
) = @field(aux_row, field.name);
|
||||
}
|
||||
return adapted_aux_row;
|
||||
}
|
||||
|
||||
fn mapAux(
|
||||
aux_query: anytype,
|
||||
aux_type: type,
|
||||
adapted_aux_row: aux_type,
|
||||
T: type,
|
||||
aux_row: T,
|
||||
comptime foreign_key: []const u8,
|
||||
MapType: type,
|
||||
map: *MapType,
|
||||
) !void {
|
||||
const foreign_key_value = @field(aux_row, foreign_key);
|
||||
const maybe_row_index = switch (@typeInfo(@TypeOf(foreign_key_value))) {
|
||||
.optional => if (foreign_key_value) |value|
|
||||
map.id_map.get(value)
|
||||
else
|
||||
null,
|
||||
else => map.id_map.get(foreign_key_value),
|
||||
};
|
||||
|
||||
if (maybe_row_index) |row_index| {
|
||||
// We pre-fill the map with an empty `MergedRow` so this is
|
||||
// guaranteed to exist (or we have a bug).
|
||||
const aux_values = map.aux_map.getEntry(row_index).?;
|
||||
try @field(
|
||||
aux_values.value_ptr.*,
|
||||
aux_query.relation.relation_name,
|
||||
).append(adapted_aux_row);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fn MergedRowType(auxiliary_queries: []const AuxiliaryQuery, ResultType: type) type {
|
||||
var fields: [auxiliary_queries.len]std.builtin.Type.StructField = undefined;
|
||||
for (auxiliary_queries, 0..) |aux_query, index| {
|
||||
fields[index] = jetquery.fields.structField(
|
||||
aux_query.relation.relation_name,
|
||||
std.ArrayList(AuxType(ResultType, aux_query.relation)),
|
||||
);
|
||||
}
|
||||
return jetquery.fields.structType(&fields);
|
||||
}
|
||||
|
||||
fn AuxType(ResultType: type, Relation: type) type {
|
||||
const field_name = std.enums.nameCast(
|
||||
std.meta.FieldEnum(ResultType),
|
||||
Relation.relation_name,
|
||||
);
|
||||
return switch (@typeInfo(std.meta.fieldInfo(ResultType, field_name).type)) {
|
||||
.pointer => |info| info.child,
|
||||
inline else => |tag| @compileError(std.fmt.comptimePrint(
|
||||
"Expected slice for relation, found: `{s}`",
|
||||
.{@tagName(tag) ++ "`"},
|
||||
)),
|
||||
};
|
||||
}
|
||||
|
||||
fn WhereArgs(
|
||||
aux_query: AuxiliaryQuery,
|
||||
comptime foreign_key: []const u8,
|
||||
arg_context: enum { one, many },
|
||||
) type {
|
||||
const field_type = jetquery.fields.fieldType(
|
||||
aux_query.relation.Source.Definition,
|
||||
foreign_key,
|
||||
);
|
||||
comptime {
|
||||
var fields: [1]std.builtin.Type.StructField = .{jetquery.fields.structField(
|
||||
foreign_key,
|
||||
switch (arg_context) {
|
||||
.one => field_type,
|
||||
.many => []const field_type,
|
||||
},
|
||||
)};
|
||||
return jetquery.fields.structType(&fields);
|
||||
}
|
||||
}
|
||||
|
||||
fn IdMap(Query: type, comptime primary_key: []const u8) type {
|
||||
const PK = if (comptime @hasField(Query.info.Model.Definition, primary_key))
|
||||
jetquery.fields.fieldType(Query.info.Model.Definition, primary_key)
|
||||
else
|
||||
void;
|
||||
return switch (PK) {
|
||||
[]const u8 => std.StringHashMap(usize),
|
||||
else => std.AutoHashMap(PK, usize),
|
||||
};
|
||||
}
|
||||
|
||||
fn PrimaryKey(Query: type, comptime primary_key: []const u8) type {
|
||||
return if (comptime @hasField(Query.info.Model.Definition, primary_key))
|
||||
jetquery.fields.fieldType(Query.info.Model.Definition, primary_key)
|
||||
else
|
||||
void;
|
||||
}
|
||||
|
||||
fn Map(QueryType: type, MergedRow: type, comptime primary_key: []const u8) type {
|
||||
return struct {
|
||||
id_array: std.ArrayList(PrimaryKey(QueryType, primary_key)),
|
||||
id_map: IdMap(QueryType, primary_key),
|
||||
aux_map: std.AutoHashMap(usize, MergedRow),
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator) @This() {
|
||||
const PK = PrimaryKey(QueryType, primary_key);
|
||||
const IM = IdMap(QueryType, primary_key);
|
||||
const AM = std.AutoHashMap(usize, MergedRow);
|
||||
return .{
|
||||
.id_array = std.ArrayList(PK).init(allocator),
|
||||
.id_map = IM.init(allocator),
|
||||
.aux_map = AM.init(allocator),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *@This()) void {
|
||||
defer self.id_array.deinit();
|
||||
defer self.id_map.deinit();
|
||||
defer self.aux_map.deinit();
|
||||
}
|
||||
};
|
||||
}
|
||||
29
src/jetquery/Row.zig
Normal file
29
src/jetquery/Row.zig
Normal file
@ -0,0 +1,29 @@
|
||||
/// A row returned in a `Result`.
|
||||
const std = @import("std");
|
||||
|
||||
const jetquery = @import("../jetquery.zig");
|
||||
|
||||
/// A row returned in a `Result`.
|
||||
allocator: std.mem.Allocator,
|
||||
values: []const jetquery.Value,
|
||||
columns: [][]const u8,
|
||||
|
||||
const Row = @This();
|
||||
|
||||
pub fn deinit(self: Row) void {
|
||||
self.allocator.free(self.values);
|
||||
}
|
||||
|
||||
/// Retrieve a typed value from a result row.
|
||||
pub fn get(self: Row, T: type, column_name: []const u8) ?T {
|
||||
for (self.columns, self.values) |column, value| {
|
||||
if (std.mem.eql(u8, column_name, column)) return switch (T) {
|
||||
[]const u8 => value.string,
|
||||
usize => value.integer,
|
||||
f64 => value.float,
|
||||
bool => value.boolean,
|
||||
else => @compileError("Unsupported type: " ++ @typeName(T)),
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
40
src/jetquery/Value.zig
Normal file
40
src/jetquery/Value.zig
Normal file
@ -0,0 +1,40 @@
|
||||
const std = @import("std");
|
||||
|
||||
const jetquery = @import("../jetquery.zig");
|
||||
|
||||
/// A bound parameter (e.g. used in a where clause).
|
||||
pub const Value = union(enum) {
|
||||
string: []const u8,
|
||||
integer: usize,
|
||||
float: f64,
|
||||
boolean: bool,
|
||||
Null: void,
|
||||
err: anyerror,
|
||||
|
||||
pub fn toSql(self: Value, buf: []u8, adapter: jetquery.adapters.Adapter, index: usize) ![]const u8 {
|
||||
return try adapter.paramSql(buf, self, index);
|
||||
// var stream = std.io.fixedBufferStream(buf);
|
||||
// switch (self) {
|
||||
// .string => |value| try writer.print("'{s}'", .{value}),
|
||||
// .integer => |value| try writer.print("{}", .{value}),
|
||||
// .float => |value| try writer.print("{d}", .{value}),
|
||||
// .boolean => |value| try writer.print("{}", .{@as(u1, if (value) 1 else 0)}),
|
||||
// .Null => try writer.print("NULL", .{}),
|
||||
// .err => |err| return err,
|
||||
// }
|
||||
// return stream.getWritten();
|
||||
}
|
||||
|
||||
pub fn eql(self: Value, other: Value) bool {
|
||||
return switch (self) {
|
||||
.string => |value| other == .string and std.mem.eql(u8, value, other.string),
|
||||
.integer => |value| other == .integer and value == other.integer,
|
||||
.float => |value| other == .float and value == other.float,
|
||||
.boolean => |value| other == .boolean and value == other.boolean,
|
||||
.Null => other == .Null,
|
||||
.err => false,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn PG() void {}
|
||||
};
|
||||
267
src/jetquery/adapters.zig
Normal file
267
src/jetquery/adapters.zig
Normal file
@ -0,0 +1,267 @@
|
||||
const std = @import("std");
|
||||
|
||||
const jetquery = @import("../jetquery.zig");
|
||||
|
||||
pub const PostgresqlAdapter = @import("adapters/PostgresqlAdapter.zig");
|
||||
pub const NullAdapter = @import("adapters/NullAdapter.zig");
|
||||
|
||||
pub const Name = enum { postgresql, null };
|
||||
|
||||
pub fn Type(adapter: Name) type {
|
||||
return switch (adapter) {
|
||||
.postgresql => PostgresqlAdapter,
|
||||
.null => NullAdapter,
|
||||
};
|
||||
}
|
||||
|
||||
pub const ConnectionOptions = struct { context: jetquery.Context };
|
||||
|
||||
pub fn Adapter(comptime adapter_name: Name, AdaptedRepo: type) type {
|
||||
const Union = union(enum) {
|
||||
postgresql: PostgresqlAdapter,
|
||||
null: NullAdapter,
|
||||
|
||||
const Self = @This();
|
||||
|
||||
pub const name = adapter_name;
|
||||
|
||||
pub fn connect(self: *Self, options: ConnectionOptions) !jetquery.Connection {
|
||||
return switch (comptime adapter_name) {
|
||||
inline else => |tag| try @field(self, @tagName(tag)).connect(options),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn release(self: *Self, connection: jetquery.Connection) void {
|
||||
return switch (comptime adapter_name) {
|
||||
inline else => |tag| @field(self, @tagName(tag)).release(connection),
|
||||
};
|
||||
}
|
||||
|
||||
/// Convert a column type to a database type suitable for the active adapter.
|
||||
pub fn columnTypeSql(self: Self, comptime column: jetquery.schema.Column) []const u8 {
|
||||
return switch (self) {
|
||||
inline else => |adapter| @TypeOf(adapter).columnTypeSql(column),
|
||||
};
|
||||
}
|
||||
|
||||
/// Quote an identifier (e.g. a table name) suitable for the active adapter.
|
||||
pub fn identifier(self: Self, comptime value: []const u8) []const u8 {
|
||||
return switch (self) {
|
||||
inline else => |adapter| @TypeOf(adapter).identifier(value),
|
||||
};
|
||||
}
|
||||
|
||||
/// Quote a column bound to a table suitable for the active adapter.
|
||||
pub fn columnSql(
|
||||
self: Self,
|
||||
comptime column: jetquery.columns.Column,
|
||||
) []const u8 {
|
||||
return switch (self) {
|
||||
inline else => |adapter| @TypeOf(adapter).columnSql(column),
|
||||
};
|
||||
}
|
||||
|
||||
/// SQL fragment used to indicate a primary key.
|
||||
pub fn primaryKeySql(self: Self, comptime column: jetquery.schema.Column) []const u8 {
|
||||
return switch (self) {
|
||||
inline else => |adapter| @TypeOf(adapter).primaryKeySql(column),
|
||||
};
|
||||
}
|
||||
|
||||
/// SQL fragment used to indicate a column whose value cannot be `NULL`.
|
||||
pub fn notNullSql(self: Self) []const u8 {
|
||||
return switch (self) {
|
||||
inline else => |adapter| @TypeOf(adapter).notNullSql(),
|
||||
};
|
||||
}
|
||||
|
||||
/// SQL representing a bind parameter, e.g. `$1`.
|
||||
pub fn paramSql(self: Self, comptime index: usize) []const u8 {
|
||||
return switch (self) {
|
||||
inline else => |adapter| @TypeOf(adapter).paramSql(index),
|
||||
};
|
||||
}
|
||||
|
||||
/// Same as `paramSql` but writes to a buffer at runtime.
|
||||
pub fn paramSqlBuf(self: Self, buf: []u8, index: usize) ![]const u8 {
|
||||
return switch (self) {
|
||||
inline else => |adapter| try @TypeOf(adapter).paramSqlBuf(buf, index),
|
||||
};
|
||||
}
|
||||
|
||||
/// SQL representing an array bind parameter with an `ANY` call, e.g. `ANY ($1)`.
|
||||
pub fn anyParamSql(self: Self, comptime index: usize) []const u8 {
|
||||
return switch (self) {
|
||||
inline else => |adapter| @TypeOf(adapter).anyParamSql(index),
|
||||
};
|
||||
}
|
||||
|
||||
/// SQL representing an `ORDER BY` directive, e.g. `"foo" DESC`
|
||||
pub fn orderSql(self: Self, comptime order_clause: jetquery.OrderClause) []const u8 {
|
||||
return switch (self) {
|
||||
inline else => |adapter| @TypeOf(adapter).orderSql(order_clause),
|
||||
};
|
||||
}
|
||||
|
||||
/// SQL fragment used when generating a `COUNT` column, e.g. `COUNT(*)`
|
||||
pub fn countSql(
|
||||
self: Self,
|
||||
comptime distinct: ?[]const jetquery.columns.Column,
|
||||
) []const u8 {
|
||||
return switch (self) {
|
||||
inline else => |adapter| @TypeOf(adapter).countSql(distinct),
|
||||
};
|
||||
}
|
||||
|
||||
/// SQL representing an inner join, e.g. `INNER JOIN "foo" ON "bar"."baz" = "foo"."baz"`
|
||||
pub fn innerJoinSql(
|
||||
self: Self,
|
||||
Model: type,
|
||||
JoinTable: type,
|
||||
comptime relation_name: []const u8,
|
||||
comptime options: JoinOptions,
|
||||
) []const u8 {
|
||||
return switch (self) {
|
||||
inline else => |adapter| @TypeOf(adapter).innerJoinSql(
|
||||
Model,
|
||||
JoinTable,
|
||||
relation_name,
|
||||
options,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/// SQL representing an outer join, e.g. `LEFT OUTER JOIN "foo" ON "bar"."baz" = "foo"."baz"`
|
||||
pub fn outerJoinSql(
|
||||
self: Self,
|
||||
Model: type,
|
||||
JoinTable: type,
|
||||
comptime relation_name: []const u8,
|
||||
comptime options: JoinOptions,
|
||||
) []const u8 {
|
||||
return switch (self) {
|
||||
inline else => |adapter| @TypeOf(adapter).outerJoinSql(
|
||||
Model,
|
||||
JoinTable,
|
||||
relation_name,
|
||||
options,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/// SQL fragment used as a `WHERE` clause when no clause has been applied by the user.
|
||||
pub fn emptyWhereSQL(self: Self) []const u8 {
|
||||
return switch (self) {
|
||||
inline else => |adapter| @TypeOf(adapter).emptyWhereSQL(),
|
||||
};
|
||||
}
|
||||
|
||||
/// Automatically generate an index name from the given table name and columns. Fails if
|
||||
/// generated name is too long for adapter's identifier length limit.
|
||||
pub fn indexName(
|
||||
self: Self,
|
||||
comptime table_name: []const u8,
|
||||
comptime column_names: []const []const u8,
|
||||
) []const u8 {
|
||||
return switch (self) {
|
||||
inline else => |adapter| @TypeOf(adapter).indexName(table_name, column_names),
|
||||
};
|
||||
}
|
||||
|
||||
/// Generate SQL for creating an index with the active adapter.
|
||||
pub fn createIndexSql(
|
||||
self: Self,
|
||||
comptime index_name: []const u8,
|
||||
comptime table_name: []const u8,
|
||||
comptime column_names: []const []const u8,
|
||||
comptime options: AdaptedRepo.CreateIndexOptions,
|
||||
) []const u8 {
|
||||
return switch (self) {
|
||||
inline else => |adapter| &@TypeOf(adapter).createIndexSql(
|
||||
index_name,
|
||||
table_name,
|
||||
column_names,
|
||||
options,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/// SQL fragment used when specifying a unique constraint.
|
||||
pub fn uniqueColumnSql(self: Self) []const u8 {
|
||||
return switch (self) {
|
||||
inline else => |adapter| @TypeOf(adapter).uniqueColumnSql(),
|
||||
};
|
||||
}
|
||||
|
||||
/// SQL fragment used to denote a foreign key.
|
||||
pub fn referenceSql(
|
||||
self: Self,
|
||||
comptime reference: jetquery.schema.Column.Reference,
|
||||
) []const u8 {
|
||||
return switch (self) {
|
||||
inline else => |adapter| @TypeOf(adapter).referenceSql(reference),
|
||||
};
|
||||
}
|
||||
|
||||
/// Resolve an appropriate type for a given aggregate function (e.g. COUNT, MIN, MAX, etc.).
|
||||
pub fn Aggregate(self: Self, context: jetquery.sql.FunctionContext) type {
|
||||
return switch (self) {
|
||||
inline else => |adapter| @TypeOf(adapter).Aggregate(context),
|
||||
};
|
||||
}
|
||||
|
||||
/// Return all metadata from the database needed to generate a schema file.
|
||||
pub fn reflect(
|
||||
self: *Self,
|
||||
allocator: std.mem.Allocator,
|
||||
repo: *AdaptedRepo,
|
||||
) !jetquery.Reflection {
|
||||
return switch (comptime adapter_name) {
|
||||
inline else => |tag| try @field(self, @tagName(tag)).reflect(allocator, repo),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn writeAddColumnSql(
|
||||
self: Self,
|
||||
comptime column: jetquery.schema.Column,
|
||||
writer: anytype,
|
||||
) !void {
|
||||
if (column.timestamps) |timestamps| {
|
||||
try timestamps.toSql(writer, self);
|
||||
} else {
|
||||
try writer.print(
|
||||
\\{s}{s}{s}{s}{s}{s}
|
||||
, .{
|
||||
self.identifier(column.name),
|
||||
if (column.primary_key)
|
||||
""
|
||||
else
|
||||
self.columnTypeSql(column),
|
||||
if (!column.primary_key and column.options.not_null)
|
||||
self.notNullSql()
|
||||
else
|
||||
"",
|
||||
if (column.primary_key) self.primaryKeySql(column) else "",
|
||||
if (column.options.unique) self.uniqueColumnSql() else "",
|
||||
if (column.options.reference) |reference|
|
||||
self.referenceSql(reference)
|
||||
else
|
||||
"",
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
return Union;
|
||||
}
|
||||
|
||||
pub const JoinOptions = struct {
|
||||
foreign_key: ?[]const u8 = null,
|
||||
primary_key: ?[]const u8 = null,
|
||||
};
|
||||
|
||||
pub const test_adapter = Adapter(.postgresql){ .postgresql = .{
|
||||
.options = undefined,
|
||||
.pool = undefined,
|
||||
.allocator = undefined,
|
||||
.connected = undefined,
|
||||
} };
|
||||
158
src/jetquery/adapters/NullAdapter.zig
Normal file
158
src/jetquery/adapters/NullAdapter.zig
Normal file
@ -0,0 +1,158 @@
|
||||
const std = @import("std");
|
||||
|
||||
const jetquery = @import("../../jetquery.zig");
|
||||
const fields = @import("../fields.zig");
|
||||
|
||||
const NullAdapter = @This();
|
||||
const AdaptedRepo = jetquery.Repo(.null, struct {});
|
||||
|
||||
pub const Options = struct {};
|
||||
|
||||
pub const name: jetquery.adapters.Name = .null;
|
||||
|
||||
pub fn execute(self: *const NullAdapter, repo: *const AdaptedRepo, sql: []const u8, values: anytype, caller_info: ?jetquery.debug.CallerInfo) !jetquery.Result {
|
||||
_ = self;
|
||||
_ = repo;
|
||||
_ = sql;
|
||||
_ = values;
|
||||
_ = caller_info;
|
||||
return error.JetQueryNullAdapterError;
|
||||
}
|
||||
|
||||
pub fn deinit(self: *const NullAdapter) void {
|
||||
_ = self;
|
||||
}
|
||||
|
||||
pub fn connect(self: *const NullAdapter, repo: *const AdaptedRepo) !jetquery.Connection {
|
||||
_ = self;
|
||||
_ = repo;
|
||||
return error.JetQueryNullAdapterError;
|
||||
}
|
||||
|
||||
pub fn release(
|
||||
self: *const NullAdapter,
|
||||
connection: jetquery.Connection,
|
||||
) void {
|
||||
// We don't return an error here because `release` is used in defers, but execute/connect
|
||||
// will error before we get here in usual circumstances.
|
||||
_ = self;
|
||||
_ = connection;
|
||||
}
|
||||
|
||||
pub fn columnTypeSql(comptime column: jetquery.schema.Column) []const u8 {
|
||||
_ = column;
|
||||
return "";
|
||||
}
|
||||
|
||||
pub fn Aggregate(context: jetquery.sql.FunctionContext) type {
|
||||
_ = context;
|
||||
return usize;
|
||||
}
|
||||
|
||||
pub fn identifier(comptime value: []const u8) []const u8 {
|
||||
_ = value;
|
||||
return "";
|
||||
}
|
||||
|
||||
pub fn columnSql(Table: type, comptime column: jetquery.columnsColumn) []const u8 {
|
||||
_ = Table;
|
||||
_ = column;
|
||||
return "";
|
||||
}
|
||||
|
||||
pub fn primaryKeySql(comptime column: jetquery.schema.Column) []const u8 {
|
||||
_ = column;
|
||||
return "";
|
||||
}
|
||||
|
||||
pub fn notNullSql() []const u8 {
|
||||
return "";
|
||||
}
|
||||
|
||||
pub fn countSql(comptime distinct: ?[]const jetquery.columns.Column) []const u8 {
|
||||
_ = distinct;
|
||||
return "";
|
||||
}
|
||||
|
||||
pub fn paramSql(comptime index: usize) []const u8 {
|
||||
_ = index;
|
||||
return "";
|
||||
}
|
||||
|
||||
pub fn anyParamSql(comptime index: usize) []const u8 {
|
||||
_ = index;
|
||||
return "";
|
||||
}
|
||||
|
||||
pub fn innerJoinSql(
|
||||
Table: type,
|
||||
JoinTable: type,
|
||||
comptime relation_name: []const u8,
|
||||
comptime options: jetquery.adapters.JoinOptions,
|
||||
) []const u8 {
|
||||
_ = Table;
|
||||
_ = JoinTable;
|
||||
_ = relation_name;
|
||||
_ = options;
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
pub fn outerJoinSql(
|
||||
Table: type,
|
||||
JoinTable: type,
|
||||
comptime relation_name: []const u8,
|
||||
comptime options: jetquery.adapters.JoinOptions,
|
||||
) []const u8 {
|
||||
_ = Table;
|
||||
_ = JoinTable;
|
||||
_ = relation_name;
|
||||
_ = options;
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
pub fn emptyWhereSql() []const u8 {
|
||||
return "";
|
||||
}
|
||||
|
||||
pub fn indexName(
|
||||
comptime table_name: []const u8,
|
||||
comptime column_names: []const []const u8,
|
||||
) [0]u8 {
|
||||
_ = table_name;
|
||||
_ = column_names;
|
||||
return .{};
|
||||
}
|
||||
|
||||
pub fn uniqueColumnSql() []const u8 {
|
||||
return "";
|
||||
}
|
||||
|
||||
pub fn referenceSql(comptime reference: jetquery.schema.Column.Reference) []const u8 {
|
||||
_ = reference;
|
||||
return "";
|
||||
}
|
||||
|
||||
pub fn createIndexSql(
|
||||
comptime index_name: []const u8,
|
||||
comptime table_name: []const u8,
|
||||
comptime column_names: []const []const u8,
|
||||
comptime options: jetquery.CreateIndexOptions,
|
||||
) [0]u8 {
|
||||
_ = index_name;
|
||||
_ = table_name;
|
||||
_ = column_names;
|
||||
_ = options;
|
||||
return .{};
|
||||
}
|
||||
|
||||
pub fn reflect(
|
||||
self: *const NullAdapter,
|
||||
allocator: std.mem.Allocator,
|
||||
repo: *const AdaptedRepo,
|
||||
) !jetquery.Reflection {
|
||||
_ = allocator;
|
||||
_ = self;
|
||||
return .{ .allocator = repo.allocator, .tables = &.{}, .columns = &.{} };
|
||||
}
|
||||
714
src/jetquery/adapters/PostgresqlAdapter.zig
Normal file
714
src/jetquery/adapters/PostgresqlAdapter.zig
Normal file
@ -0,0 +1,714 @@
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
|
||||
const pg = @import("pg");
|
||||
|
||||
const jetquery = @import("../../jetquery.zig");
|
||||
|
||||
const PostgresqlAdapter = @This();
|
||||
|
||||
pool: *pg.Pool,
|
||||
allocator: std.mem.Allocator,
|
||||
options: Options,
|
||||
connected: bool,
|
||||
lazy_connect: bool = false,
|
||||
|
||||
pub const Count = i64;
|
||||
pub const Average = i64;
|
||||
pub const Sum = i64;
|
||||
pub const Max = i32;
|
||||
pub const Min = i32;
|
||||
pub const max_identifier_len = 63;
|
||||
|
||||
pub const name: jetquery.adapters.Name = .postgresql;
|
||||
|
||||
pub fn Aggregate(comptime context: jetquery.sql.FunctionContext) type {
|
||||
return switch (context) {
|
||||
.min => Min,
|
||||
.max => Max,
|
||||
.count => Count,
|
||||
.avg => Average,
|
||||
.sum => Sum,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn Result(AdaptedRepo: type) type {
|
||||
return struct {
|
||||
result: *pg.Result,
|
||||
allocator: std.mem.Allocator,
|
||||
connection: *pg.Conn,
|
||||
caller_info: ?jetquery.debug.CallerInfo,
|
||||
duration: i64,
|
||||
repo: *AdaptedRepo,
|
||||
|
||||
const Self = @This();
|
||||
|
||||
pub fn deinit(self: *Self) void {
|
||||
self.result.deinit();
|
||||
}
|
||||
|
||||
pub fn drain(self: *Self) !void {
|
||||
try self.result.drain();
|
||||
}
|
||||
|
||||
pub fn next(self: *Self, query: anytype) !?@TypeOf(query).ResultType {
|
||||
if (try self.result.next()) |row| {
|
||||
var result_row: @TypeOf(query).ResultType = undefined;
|
||||
inline for (@TypeOf(query).ColumnInfos) |column_info| {
|
||||
if (column_info.relation) |relation| {
|
||||
@field(
|
||||
@field(result_row, relation.relation_name),
|
||||
column_info.name,
|
||||
) = try resolvedValue(self.allocator, column_info, &row);
|
||||
} else {
|
||||
@field(result_row, column_info.name) = try resolvedValue(
|
||||
self.allocator,
|
||||
column_info,
|
||||
&row,
|
||||
);
|
||||
}
|
||||
}
|
||||
return result_row;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unary(self: *Self, T: type) !T {
|
||||
// This error should really never happen if used in conjunction with (e.g.) a `COUNT`
|
||||
// query, but we return an error to allow the host app (e.g. Jetzig) to handle it instead
|
||||
// of panicking.
|
||||
const row = try self.result.next() orelse return error.JetQueryMissingRowInUnaryQuery;
|
||||
|
||||
if (row.values.len < 1) return error.JetQueryMissingColumnInUnaryQuery;
|
||||
|
||||
return row.get(T, 0);
|
||||
}
|
||||
|
||||
pub fn all(self: *Self, query: anytype) ![]@TypeOf(query).ResultType {
|
||||
defer self.deinit();
|
||||
|
||||
var array = std.ArrayList(@TypeOf(query).ResultType).init(self.allocator);
|
||||
while (try self.next(query)) |row| try array.append(row);
|
||||
try self.drain();
|
||||
return try array.toOwnedSlice();
|
||||
}
|
||||
|
||||
pub fn execute(
|
||||
self: *Self,
|
||||
sql: []const u8,
|
||||
values: anytype,
|
||||
) !jetquery.Result(AdaptedRepo) {
|
||||
return try self.connection.execute(sql, values, self.caller_info);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fn resolvedValue(
|
||||
allocator: std.mem.Allocator,
|
||||
column_info: jetquery.sql.ColumnInfo,
|
||||
row: *const pg.Row,
|
||||
) !column_info.type {
|
||||
return switch (column_info.type) {
|
||||
// TODO: pg.Numeric, pg.Cidr
|
||||
u8,
|
||||
?u8,
|
||||
i16,
|
||||
?i16,
|
||||
i32,
|
||||
?i32,
|
||||
i64,
|
||||
?i64,
|
||||
f32,
|
||||
?f32,
|
||||
[]u8,
|
||||
?[]u8,
|
||||
bool,
|
||||
?bool,
|
||||
[]const u8,
|
||||
?[]const u8,
|
||||
=> |T| try maybeDupe(allocator, T, row.get(T, column_info.index)),
|
||||
jetquery.jetcommon.types.DateTime => |T| try T.fromUnix(
|
||||
row.get(i64, column_info.index),
|
||||
.microseconds,
|
||||
),
|
||||
else => |T| @compileError("Unsupported type: " ++ @typeName(T)),
|
||||
};
|
||||
}
|
||||
|
||||
fn maybeDupe(allocator: std.mem.Allocator, T: type, value: T) !T {
|
||||
return switch (T) {
|
||||
[]const u8 => try allocator.dupe(u8, value),
|
||||
?[]const u8 => if (value) |val| try allocator.dupe(u8, val) else null,
|
||||
else => value,
|
||||
};
|
||||
}
|
||||
|
||||
pub const Options = struct {
|
||||
database: ?[]const u8 = null,
|
||||
username: ?[]const u8 = null,
|
||||
password: ?[]const u8 = null,
|
||||
hostname: ?[]const u8 = null,
|
||||
port: ?u16 = null,
|
||||
pool_size: ?u16 = null,
|
||||
timeout: ?u32 = null,
|
||||
|
||||
pub fn defaultValue(T: type, comptime field_name: []const u8) T {
|
||||
const tag = std.enums.nameCast(std.meta.FieldEnum(Options), field_name);
|
||||
return switch (tag) {
|
||||
.database, .username, .password => null,
|
||||
.hostname => "localhost",
|
||||
.port => 5432,
|
||||
.pool_size => 8,
|
||||
.timeout => 10_000,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/// Initialize a new PostgreSQL adapter and connection pool.
|
||||
pub fn init(allocator: std.mem.Allocator, options: Options, lazy_connect: bool) !PostgresqlAdapter {
|
||||
if (lazy_connect) return .{
|
||||
.allocator = allocator,
|
||||
.options = options,
|
||||
.pool = undefined,
|
||||
.lazy_connect = true,
|
||||
.connected = false,
|
||||
};
|
||||
|
||||
return .{
|
||||
.allocator = allocator,
|
||||
.options = options,
|
||||
.pool = try initPool(allocator, options),
|
||||
.connected = true,
|
||||
};
|
||||
}
|
||||
|
||||
/// Close connections and free resources.
|
||||
pub fn deinit(self: *PostgresqlAdapter) void {
|
||||
self.pool.deinit();
|
||||
}
|
||||
|
||||
pub const Connection = struct {
|
||||
connection: *pg.Conn,
|
||||
options: jetquery.adapters.ConnectionOptions,
|
||||
|
||||
pub fn execute(
|
||||
self: Connection,
|
||||
sql: []const u8,
|
||||
values: anytype,
|
||||
caller_info: ?jetquery.debug.CallerInfo,
|
||||
repo: anytype,
|
||||
) !jetquery.Result(@TypeOf(repo.*)) {
|
||||
const start_time = std.time.nanoTimestamp();
|
||||
|
||||
const result = self.connection.queryOpts(sql, values, .{}) catch |err| {
|
||||
try self.errorCallback(err, sql, repo, caller_info);
|
||||
return err;
|
||||
};
|
||||
|
||||
const duration: i64 = @intCast(std.time.nanoTimestamp() - start_time);
|
||||
|
||||
try repo.eventCallback(.{
|
||||
.sql = sql,
|
||||
.caller_info = caller_info,
|
||||
.duration = duration,
|
||||
.context = self.options.context,
|
||||
});
|
||||
|
||||
return .{
|
||||
.postgresql = .{
|
||||
.allocator = repo.allocator,
|
||||
.result = result,
|
||||
.connection = self.connection,
|
||||
.caller_info = caller_info,
|
||||
.duration = duration,
|
||||
.repo = repo,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/// Execute a query with runtime binding. Used internally by `Repo.save`. This API is not
|
||||
/// intended for public use.
|
||||
pub fn executeRuntimeBind(
|
||||
self: Connection,
|
||||
sql: []const u8,
|
||||
values: anytype,
|
||||
comptime Args: type,
|
||||
args: Args,
|
||||
field_states: []const jetquery.sql.FieldState,
|
||||
caller_info: ?jetquery.debug.CallerInfo,
|
||||
repo: anytype,
|
||||
) !jetquery.Result(@TypeOf(repo.*)) {
|
||||
const start_time = std.time.nanoTimestamp();
|
||||
|
||||
var stmt = try pg.Stmt.init(self.connection, .{});
|
||||
errdefer stmt.deinit();
|
||||
|
||||
stmt.prepare(sql) catch |err| {
|
||||
try self.errorCallback(err, sql, repo, caller_info);
|
||||
return err;
|
||||
};
|
||||
|
||||
inline for (values) |value| {
|
||||
try stmt.bind(value);
|
||||
}
|
||||
|
||||
inline for (std.meta.fields(Args), 0..) |field, index| {
|
||||
if (field_states[index].modified) try stmt.bind(@field(args, field.name));
|
||||
}
|
||||
|
||||
const result = stmt.execute() catch |err| {
|
||||
try self.errorCallback(err, sql, repo, caller_info);
|
||||
return err;
|
||||
};
|
||||
|
||||
const duration: i64 = @intCast(std.time.nanoTimestamp() - start_time);
|
||||
|
||||
try repo.eventCallback(.{
|
||||
.sql = sql,
|
||||
.caller_info = caller_info,
|
||||
.duration = duration,
|
||||
.context = self.options.context,
|
||||
});
|
||||
|
||||
return .{
|
||||
.postgresql = .{
|
||||
.allocator = repo.allocator,
|
||||
.result = result,
|
||||
.connection = self.connection,
|
||||
.caller_info = caller_info,
|
||||
.duration = duration,
|
||||
.repo = repo,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
pub fn release(self: Connection) void {
|
||||
self.connection.release();
|
||||
}
|
||||
|
||||
fn errorCallback(
|
||||
self: Connection,
|
||||
err: anyerror,
|
||||
sql: []const u8,
|
||||
repo: anytype,
|
||||
caller_info: ?jetquery.debug.CallerInfo,
|
||||
) !void {
|
||||
if (self.connection.err) |connection_error| {
|
||||
try repo.eventCallback(.{
|
||||
.sql = sql,
|
||||
.err = .{ .err = err, .message = connection_error.message },
|
||||
.status = .fail,
|
||||
.caller_info = caller_info,
|
||||
.context = self.options.context,
|
||||
});
|
||||
} else {
|
||||
try repo.eventCallback(.{
|
||||
.sql = sql,
|
||||
.err = .{ .err = err, .message = "[unknown error]" },
|
||||
.status = .fail,
|
||||
.caller_info = caller_info,
|
||||
.context = self.options.context,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
pub fn connect(
|
||||
self: *PostgresqlAdapter,
|
||||
options: jetquery.adapters.ConnectionOptions,
|
||||
) !jetquery.Connection {
|
||||
if (self.lazy_connect) self.pool = try initPool(self.allocator, self.options);
|
||||
return .{ .postgresql = .{ .options = options, .connection = try self.pool.acquire() } };
|
||||
}
|
||||
|
||||
pub fn release(self: *PostgresqlAdapter, connection: jetquery.Connection) void {
|
||||
self.pool.release(connection.postgresql.connection);
|
||||
}
|
||||
|
||||
/// Output column type as SQL.
|
||||
pub fn columnTypeSql(comptime column: jetquery.schema.Column) []const u8 {
|
||||
return switch (column.type) {
|
||||
.string => " VARCHAR" ++ std.fmt.comptimePrint("({})", .{column.options.length orelse 255}),
|
||||
.integer => " INTEGER",
|
||||
.boolean => " BOOLEAN",
|
||||
.float => " REAL",
|
||||
.decimal => " NUMERIC",
|
||||
.datetime => " TIMESTAMP",
|
||||
.text => " TEXT",
|
||||
};
|
||||
}
|
||||
|
||||
/// Output quoted identifier.
|
||||
pub fn identifier(comptime value: []const u8) []const u8 {
|
||||
return std.fmt.comptimePrint(
|
||||
\\"{s}"
|
||||
, .{value});
|
||||
}
|
||||
|
||||
/// SQL fragment used to represent a column bound to a table, e.g. `"foo"."bar"`
|
||||
pub fn columnSql(comptime column: jetquery.columns.Column) []const u8 {
|
||||
return if (column.function) |function|
|
||||
std.fmt.comptimePrint(
|
||||
\\{s}("{s}"."{s}")
|
||||
, .{
|
||||
switch (function) {
|
||||
.min => "MIN",
|
||||
.max => "MAX",
|
||||
.count => "COUNT",
|
||||
.avg => "AVG",
|
||||
.sum => "SUM",
|
||||
},
|
||||
column.table.name,
|
||||
column.name,
|
||||
})
|
||||
else if (column.sql) |sql|
|
||||
sql
|
||||
else
|
||||
std.fmt.comptimePrint(
|
||||
\\"{s}"."{s}"
|
||||
, .{ column.table.name, column.name });
|
||||
}
|
||||
|
||||
/// SQL fragment used to indicate a primary key.
|
||||
pub fn primaryKeySql(comptime column: jetquery.schema.Column) []const u8 {
|
||||
return switch (column.type) {
|
||||
.integer => " SERIAL PRIMARY KEY",
|
||||
else => comptime columnTypeSql(column) ++ " PRIMARY KEY",
|
||||
};
|
||||
}
|
||||
|
||||
/// SQL fragment used to indicate a column whose value cannot be `NULL`.
|
||||
pub fn notNullSql() []const u8 {
|
||||
return " NOT NULL";
|
||||
}
|
||||
|
||||
/// SQL representing a bind parameter, e.g. `$1`.
|
||||
pub fn paramSql(comptime index: usize) []const u8 {
|
||||
return std.fmt.comptimePrint("${}", .{index + 1});
|
||||
}
|
||||
|
||||
/// SQL representing a bind parameter, e.g. `$1`.
|
||||
pub fn paramSqlBuf(buf: []u8, index: usize) ![]const u8 {
|
||||
return try std.fmt.bufPrint(buf, "${}", .{index + 1});
|
||||
}
|
||||
|
||||
/// SQL representing an array bind parameter with an `ANY` call, e.g. `ANY ($1)`.
|
||||
pub fn anyParamSql(comptime index: usize) []const u8 {
|
||||
return std.fmt.comptimePrint("ANY (${})", .{index + 1});
|
||||
}
|
||||
|
||||
pub fn orderSql(comptime order_clause: jetquery.sql.OrderClause) []const u8 {
|
||||
const direction = switch (order_clause.direction) {
|
||||
.ascending, .asc => "ASC",
|
||||
.descending, .desc => "DESC",
|
||||
};
|
||||
|
||||
return std.fmt.comptimePrint(
|
||||
"{s} {s}",
|
||||
.{ columnSql(order_clause.column), direction },
|
||||
);
|
||||
}
|
||||
|
||||
pub fn countSql(comptime distinct: ?[]const jetquery.columns.Column) []const u8 {
|
||||
// TODO: Move some of this back into `sql.zig`.
|
||||
return if (comptime distinct) |distinct_columns| blk: {
|
||||
const template = "{s}.{s}{s}";
|
||||
var size: usize = 0;
|
||||
for (distinct_columns, 0..) |column, index| {
|
||||
size += std.fmt.count(
|
||||
template,
|
||||
.{
|
||||
identifier(column.table.name),
|
||||
identifier(column.name),
|
||||
if (index + 1 < distinct_columns.len) ", " else "",
|
||||
},
|
||||
);
|
||||
}
|
||||
var buf: [size]u8 = undefined;
|
||||
var cursor: usize = 0;
|
||||
for (distinct_columns, 0..) |column, index| {
|
||||
const column_sql = std.fmt.comptimePrint(
|
||||
template,
|
||||
.{
|
||||
identifier(column.table.name),
|
||||
identifier(column.name),
|
||||
if (index + 1 < distinct_columns.len) ", " else "",
|
||||
},
|
||||
);
|
||||
@memcpy(buf[cursor .. cursor + column_sql.len], column_sql);
|
||||
cursor += column_sql.len;
|
||||
}
|
||||
break :blk std.fmt.comptimePrint("COUNT(DISTINCT({s}))", .{buf});
|
||||
} else "COUNT(*)";
|
||||
}
|
||||
|
||||
pub fn innerJoinSql(
|
||||
Table: type,
|
||||
JoinTable: type,
|
||||
comptime relation_name: []const u8,
|
||||
comptime options: jetquery.adapters.JoinOptions,
|
||||
) []const u8 {
|
||||
const foreign_key = options.foreign_key orelse relation_name ++ "_id";
|
||||
const primary_key = options.primary_key orelse "id";
|
||||
|
||||
return std.fmt.comptimePrint(
|
||||
\\ INNER JOIN "{s}" ON "{s}"."{s}" = "{s}"."{s}"
|
||||
,
|
||||
.{
|
||||
JoinTable.name,
|
||||
Table.name,
|
||||
foreign_key,
|
||||
JoinTable.name,
|
||||
primary_key,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
pub fn outerJoinSql(
|
||||
Table: type,
|
||||
JoinTable: type,
|
||||
comptime relation_name: []const u8,
|
||||
comptime options: jetquery.adapters.JoinOptions,
|
||||
) []const u8 {
|
||||
const foreign_key = options.foreign_key orelse relation_name ++ "_id";
|
||||
const primary_key = options.primary_key orelse "id";
|
||||
|
||||
return std.fmt.comptimePrint(
|
||||
\\ LEFT OUTER JOIN "{s}" ON "{s}"."{s}" = "{s}"."{s}"
|
||||
,
|
||||
.{
|
||||
JoinTable.name,
|
||||
Table.name,
|
||||
foreign_key,
|
||||
JoinTable.name,
|
||||
primary_key,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
pub fn emptyWhereSql() []const u8 {
|
||||
return "(1 = 1)";
|
||||
}
|
||||
|
||||
pub fn indexName(
|
||||
comptime table_name: []const u8,
|
||||
comptime column_names: []const []const u8,
|
||||
) *const [indexNameSize(table_name, column_names)]u8 {
|
||||
comptime {
|
||||
var buf: [indexNameSize(table_name, column_names)]u8 = undefined;
|
||||
const prefix = std.fmt.comptimePrint("index_{s}_", .{table_name});
|
||||
@memcpy(buf[0..prefix.len], prefix);
|
||||
|
||||
var cursor: usize = prefix.len;
|
||||
for (column_names, 0..) |column_name, index| {
|
||||
const separator = if (index + 1 < column_names.len) "_" else "";
|
||||
const column_suffix = std.fmt.comptimePrint("{s}{s}", .{ column_name, separator });
|
||||
@memcpy(buf[cursor .. cursor + column_suffix.len], column_suffix);
|
||||
cursor += column_suffix.len;
|
||||
}
|
||||
const final = buf;
|
||||
return &final;
|
||||
}
|
||||
}
|
||||
|
||||
fn indexNameSize(comptime table_name: []const u8, comptime column_names: []const []const u8) usize {
|
||||
comptime {
|
||||
var size: usize = 0;
|
||||
size += std.fmt.comptimePrint("index_{s}_", .{table_name}).len;
|
||||
for (column_names, 0..) |column_name, index| {
|
||||
const separator = if (index + 1 < column_names.len) "_" else "";
|
||||
size += std.fmt.comptimePrint("{s}{s}", .{ column_name, separator }).len;
|
||||
}
|
||||
if (size > max_identifier_len) {
|
||||
@compileError(
|
||||
std.fmt.comptimePrint(
|
||||
"Generated index name length {} longer than {} characters. Specify `.index_name` to manually set a name for this index.",
|
||||
.{ size, max_identifier_len },
|
||||
),
|
||||
);
|
||||
}
|
||||
return size;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn createIndexSql(
|
||||
comptime index_name: []const u8,
|
||||
comptime table_name: []const u8,
|
||||
comptime column_names: []const []const u8,
|
||||
comptime options: jetquery.CreateIndexOptions,
|
||||
) *const [createIndexSqlSize(index_name, table_name, column_names, options)]u8 {
|
||||
comptime {
|
||||
var buf: [createIndexSqlSize(index_name, table_name, column_names, options)]u8 = undefined;
|
||||
const statement = std.fmt.comptimePrint(
|
||||
"CREATE {s}INDEX{s} {s} ON {s} (",
|
||||
.{
|
||||
if (options.unique) "UNIQUE " else "",
|
||||
if (options.if_not_exists) " IF NOT EXISTS" else "",
|
||||
identifier(index_name),
|
||||
identifier(table_name),
|
||||
},
|
||||
);
|
||||
@memcpy(buf[0..statement.len], statement);
|
||||
|
||||
var cursor: usize = statement.len;
|
||||
for (column_names, 0..) |column_name, index| {
|
||||
const separator = if (index + 1 < column_names.len) ", " else "";
|
||||
const column = std.fmt.comptimePrint("{s}{s}", .{ column_name, separator });
|
||||
@memcpy(buf[cursor .. cursor + column.len], column);
|
||||
cursor += column.len;
|
||||
}
|
||||
buf[cursor] = ')';
|
||||
const final = buf;
|
||||
return &final;
|
||||
}
|
||||
}
|
||||
|
||||
fn createIndexSqlSize(
|
||||
comptime index_name: []const u8,
|
||||
comptime table_name: []const u8,
|
||||
comptime column_names: []const []const u8,
|
||||
comptime options: jetquery.CreateIndexOptions,
|
||||
) usize {
|
||||
comptime {
|
||||
var size: usize = 0;
|
||||
size += std.fmt.comptimePrint(
|
||||
"CREATE {s}INDEX{s} {s} ON {s} (",
|
||||
.{
|
||||
if (options.unique) "UNIQUE " else "",
|
||||
if (options.if_not_exists) " IF NOT EXISTS" else "",
|
||||
identifier(index_name),
|
||||
identifier(table_name),
|
||||
},
|
||||
).len;
|
||||
for (column_names, 0..) |column_name, index| {
|
||||
const separator = if (index + 1 < column_names.len) ", " else "";
|
||||
size += std.fmt.comptimePrint("{s}{s}", .{ column_name, separator }).len;
|
||||
}
|
||||
size += ")".len;
|
||||
return size;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn uniqueColumnSql() []const u8 {
|
||||
return " UNIQUE";
|
||||
}
|
||||
|
||||
pub fn referenceSql(comptime reference: jetquery.schema.Column.Reference) []const u8 {
|
||||
return std.fmt.comptimePrint(
|
||||
" REFERENCES {s}({s})",
|
||||
.{ comptime identifier(reference[0]), comptime identifier(reference[1]) },
|
||||
);
|
||||
}
|
||||
|
||||
pub fn reflect(
|
||||
self: *PostgresqlAdapter,
|
||||
allocator: std.mem.Allocator,
|
||||
repo: anytype,
|
||||
) !jetquery.Reflection {
|
||||
const tables = try self.reflectTables(allocator, repo);
|
||||
const columns = try self.reflectColumns(allocator, repo);
|
||||
return .{ .allocator = self.allocator, .tables = tables, .columns = columns };
|
||||
}
|
||||
|
||||
pub fn reflectTables(
|
||||
self: *PostgresqlAdapter,
|
||||
allocator: std.mem.Allocator,
|
||||
repo: anytype,
|
||||
) ![]const jetquery.Reflection.TableInfo {
|
||||
_ = self;
|
||||
|
||||
const sql =
|
||||
\\SELECT "table_name" FROM "information_schema"."tables" WHERE "table_schema" = 'public' AND "table_name" <> 'jetquery_migrations' ORDER BY "table_name"
|
||||
;
|
||||
var result = try repo.executeSql(sql, .{});
|
||||
defer result.deinit();
|
||||
|
||||
var tables = std.ArrayList(jetquery.Reflection.TableInfo).init(allocator);
|
||||
while (try result.postgresql.result.next()) |row| {
|
||||
try tables.append(.{
|
||||
.name = try allocator.dupe(u8, row.get([]const u8, 0)),
|
||||
});
|
||||
}
|
||||
try result.drain();
|
||||
|
||||
return try tables.toOwnedSlice();
|
||||
}
|
||||
|
||||
pub fn reflectColumns(
|
||||
_: *PostgresqlAdapter,
|
||||
allocator: std.mem.Allocator,
|
||||
repo: anytype,
|
||||
) ![]const jetquery.Reflection.ColumnInfo {
|
||||
const sql =
|
||||
\\SELECT "table_name", "column_name", "data_type", "is_nullable" FROM "information_schema"."columns" WHERE "table_schema" = 'public' ORDER BY "table_name", "ordinal_position"
|
||||
;
|
||||
var result = try repo.executeSql(sql, .{});
|
||||
defer result.deinit();
|
||||
|
||||
var columns = std.ArrayList(jetquery.Reflection.ColumnInfo).init(allocator);
|
||||
while (try result.postgresql.result.next()) |row| {
|
||||
try columns.append(.{
|
||||
.table = try allocator.dupe(u8, row.get([]const u8, 0)),
|
||||
.name = try allocator.dupe(u8, row.get([]const u8, 1)),
|
||||
.type = translateColumnType(row.get([]const u8, 2)),
|
||||
.null = std.mem.eql(u8, row.get([]const u8, 3), "YES"),
|
||||
});
|
||||
}
|
||||
try result.drain();
|
||||
|
||||
return try columns.toOwnedSlice();
|
||||
}
|
||||
|
||||
fn translateColumnType(column_name: []const u8) jetquery.schema.Column.Type {
|
||||
// TODO
|
||||
const types = std.StaticStringMap(jetquery.schema.Column.Type).initComptime(.{
|
||||
.{ "integer", jetquery.schema.Column.Type.integer },
|
||||
.{ "real", jetquery.schema.Column.Type.float },
|
||||
.{ "boolean", jetquery.schema.Column.Type.boolean },
|
||||
.{ "numeric", jetquery.schema.Column.Type.decimal },
|
||||
.{ "character varying", jetquery.schema.Column.Type.string },
|
||||
.{ "text", jetquery.schema.Column.Type.text },
|
||||
.{ "timestamp without time zone", jetquery.schema.Column.Type.datetime },
|
||||
.{ "timestamp with time zone", jetquery.schema.Column.Type.datetime },
|
||||
});
|
||||
return types.get(column_name) orelse {
|
||||
std.log.err("Unsupported column type: `{s}`\n", .{column_name});
|
||||
unreachable;
|
||||
};
|
||||
}
|
||||
|
||||
fn initPool(allocator: std.mem.Allocator, options: Options) !*pg.Pool {
|
||||
return try pg.Pool.init(allocator, .{
|
||||
.size = options.pool_size.?,
|
||||
.connect = .{
|
||||
.port = options.port.?,
|
||||
.host = options.hostname.?,
|
||||
},
|
||||
.auth = .{
|
||||
.username = options.username orelse return configError("username"),
|
||||
.database = options.database orelse return configError("database"),
|
||||
.password = options.password orelse return configError("password"),
|
||||
.timeout = options.timeout.?,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
fn configError(comptime config_field: []const u8) error{JetQueryConfigError} {
|
||||
const template = "Missing database configuration value for: `{s}`. " ++
|
||||
"Configure in JetQuery config file or `JETQUERY_{s}`.";
|
||||
const message = comptime blk: {
|
||||
var buf: [config_field.len]u8 = undefined;
|
||||
break :blk std.fmt.comptimePrint(
|
||||
template,
|
||||
.{ config_field, std.ascii.upperString(&buf, config_field) },
|
||||
);
|
||||
};
|
||||
|
||||
if (builtin.is_test) { // https://github.com/ziglang/zig/issues/5738
|
||||
std.log.warn(message, .{});
|
||||
} else {
|
||||
std.log.err(message, .{});
|
||||
}
|
||||
return error.JetQueryConfigError;
|
||||
}
|
||||
162
src/jetquery/coercion.zig
Normal file
162
src/jetquery/coercion.zig
Normal file
@ -0,0 +1,162 @@
|
||||
const std = @import("std");
|
||||
|
||||
const jetcommon = @import("jetcommon");
|
||||
|
||||
const fields = @import("fields.zig");
|
||||
|
||||
pub fn coerce(
|
||||
Table: type,
|
||||
field_info: fields.FieldInfo,
|
||||
value: anytype,
|
||||
) CoercedValue(fields.ColumnType(Table, field_info), @TypeOf(value)) {
|
||||
switch (field_info.context) {
|
||||
.limit, .offset => return switch (@typeInfo(@TypeOf(value))) {
|
||||
.int, .comptime_int => .{ .value = value },
|
||||
else => coerceDelegate(usize, value),
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
|
||||
const T = fields.ColumnType(Table, field_info);
|
||||
|
||||
if (T == jetcommon.types.DateTime) return value.microseconds;
|
||||
|
||||
return switch (@typeInfo(@TypeOf(value))) {
|
||||
.null => .{ .value = null },
|
||||
.int, .comptime_int => switch (@typeInfo(T)) {
|
||||
.int => .{ .value = @intCast(value) },
|
||||
else => coerceDelegate(T, value),
|
||||
},
|
||||
.float, .comptime_float => switch (@typeInfo(T)) {
|
||||
.float => .{ .value = @floatCast(value) },
|
||||
else => coerceDelegate(T, value),
|
||||
},
|
||||
.pointer => |info| switch (@typeInfo(T)) {
|
||||
.int => switch (@typeInfo(info.child)) {
|
||||
.int => switch (info.size) {
|
||||
// FIXME: For now we get away with this because postgres does not have a u8
|
||||
// type but we may need a better way to identify strings if another database
|
||||
// adapter does support u8.
|
||||
.Slice => if (@TypeOf(value) == []const u8)
|
||||
coerceInt(T, value)
|
||||
else
|
||||
.{ .value = value },
|
||||
else => .{ .value = value },
|
||||
},
|
||||
else => if (comptime canCoerceDelegate(info.child))
|
||||
coerceDelegate(T, value.*)
|
||||
else
|
||||
coerceInt(T, value),
|
||||
},
|
||||
.float => switch (@typeInfo(info.child)) {
|
||||
.float => switch (info.size) {
|
||||
.Slice => .{ .value = value },
|
||||
else => .{ .value = value },
|
||||
},
|
||||
else => if (comptime canCoerceDelegate(info.child))
|
||||
coerceDelegate(T, value.*)
|
||||
else
|
||||
coerceFloat(T, value),
|
||||
},
|
||||
.bool => switch (@typeInfo(info.child)) {
|
||||
.bool => switch (info.size) {
|
||||
.Slice => .{ .value = value },
|
||||
else => .{ .value = value },
|
||||
},
|
||||
else => if (comptime canCoerceDelegate(info.child))
|
||||
coerceDelegate(T, value.*)
|
||||
else
|
||||
coerceBool(T, value),
|
||||
},
|
||||
.pointer => if (comptime canCoerceDelegate(info.child))
|
||||
coerceDelegate(T, value.*)
|
||||
else
|
||||
.{ .value = value }, // Let Zig compiler figure it out
|
||||
else => if (comptime canCoerceDelegate(info.child))
|
||||
coerceDelegate(T, value.*)
|
||||
else
|
||||
@compileError("Incompatible types: `" ++
|
||||
@typeName(T) ++ "` and `" ++ @typeName(info.child) ++ "`"),
|
||||
},
|
||||
else => coerceDelegate(T, value),
|
||||
};
|
||||
}
|
||||
|
||||
// Call `toJetQuery` with a given type and an allocator on the given arg field. Although
|
||||
// this function expects a return value not specific to JetQuery, the intention is that
|
||||
// arbitrary types can implement `toJetQuery` if the author wants them to be used with
|
||||
// JetQuery, otherwise a typical Zig compile error will occur. This feature is used by
|
||||
// Zmpl for converting Zmpl Values, allowing e.g. request params in Jetzig to be used as
|
||||
// JetQuery whereclause/etc. params.
|
||||
pub fn coerceDelegate(Target: type, value: anytype) CoercedValue(Target, @TypeOf(value)) {
|
||||
const Source = @TypeOf(value);
|
||||
if (comptime canCoerceDelegate(Source)) {
|
||||
const coerced = value.toJetQuery(Target) catch |err| {
|
||||
return .{ .err = err };
|
||||
};
|
||||
return .{ .value = coerced, .err = null };
|
||||
} else {
|
||||
@compileError("Incompatible types: `" ++ @typeName(Target) ++ "` and `" ++ @typeName(Source) ++ "`");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn canCoerceDelegate(T: type) bool {
|
||||
return switch (@typeInfo(T)) {
|
||||
.@"struct", .@"union" => std.meta.hasFn(T, "toJetQuery"),
|
||||
.pointer => |info| std.meta.hasFn(info.child, "toJetQuery"),
|
||||
else => false,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn CoercedValue(Target: type, Source: type) type {
|
||||
const T = switch (@typeInfo(Source)) {
|
||||
.null => @TypeOf(null),
|
||||
.pointer => |info| if (info.child == Target and info.size == .Slice)
|
||||
[]const Target
|
||||
else
|
||||
Target,
|
||||
else => Target,
|
||||
};
|
||||
|
||||
return struct {
|
||||
value: T = undefined, // Never used if `err` is present
|
||||
err: ?anyerror = null,
|
||||
};
|
||||
}
|
||||
|
||||
fn coerceInt(T: type, value: []const u8) CoercedValue(T, @TypeOf(value)) {
|
||||
const coerced = std.fmt.parseInt(T, value, 10) catch |err| {
|
||||
return .{
|
||||
.err = switch (err) {
|
||||
error.InvalidCharacter, error.Overflow => error.JetQueryInvalidIntegerString,
|
||||
},
|
||||
};
|
||||
};
|
||||
return .{ .value = coerced };
|
||||
}
|
||||
|
||||
fn coerceFloat(T: type, value: []const u8) CoercedValue(T, @TypeOf(value)) {
|
||||
const coerced = std.fmt.parseFloat(T, value) catch |err| {
|
||||
return .{
|
||||
.err = switch (err) {
|
||||
error.InvalidCharacter => error.JetQueryInvalidFloatString,
|
||||
},
|
||||
};
|
||||
};
|
||||
return .{ .value = coerced };
|
||||
}
|
||||
|
||||
fn coerceBool(T: type, value: []const u8) CoercedValue(T, @TypeOf(value)) {
|
||||
if (value.len != 1) return .{ .err = error.JetQueryInvalidBooleanString };
|
||||
|
||||
const maybe_boolean = switch (value[0]) {
|
||||
'1' => true,
|
||||
'0' => false,
|
||||
else => null,
|
||||
};
|
||||
|
||||
return if (maybe_boolean) |boolean|
|
||||
.{ .value = boolean }
|
||||
else
|
||||
.{ .err = error.JetQueryInvalidBooleanString };
|
||||
}
|
||||
197
src/jetquery/columns.zig
Normal file
197
src/jetquery/columns.zig
Normal file
@ -0,0 +1,197 @@
|
||||
const std = @import("std");
|
||||
|
||||
const sql = @import("sql.zig");
|
||||
|
||||
pub const Column = struct {
|
||||
name: []const u8,
|
||||
type: type,
|
||||
table: type,
|
||||
function: ?sql.FunctionContext = null,
|
||||
alias: ?[]const u8 = null,
|
||||
sql: ?[]const u8 = null,
|
||||
|
||||
pub fn ResultType(comptime self: Column, Adapter: type) type {
|
||||
return if (self.function) |function| Adapter.Aggregate(function) else self.type;
|
||||
}
|
||||
|
||||
pub fn as(comptime self: Column, comptime alias: anytype) Column {
|
||||
var column = self;
|
||||
column.alias = @tagName(alias);
|
||||
return column;
|
||||
}
|
||||
};
|
||||
|
||||
pub fn translate(
|
||||
Table: type,
|
||||
relations: []const type,
|
||||
comptime maybe_args: anytype,
|
||||
) [sizeOf(Table, relations, maybe_args)]Column {
|
||||
comptime {
|
||||
const args = if (@TypeOf(maybe_args) == @TypeOf(null)) return .{} else maybe_args;
|
||||
|
||||
if (args.len == 0) return Table.columns();
|
||||
|
||||
var fields: [sizeOf(Table, relations, args)]Column = undefined;
|
||||
var index: usize = 0;
|
||||
|
||||
for (args) |arg| {
|
||||
if (@TypeOf(arg) == Column) {
|
||||
if (arg.alias == null) @compileError(std.fmt.comptimePrint(
|
||||
\\Custom SQL columns must be aliased. Call `as("...")` to specify an alias. Failed for column `{?s}`,
|
||||
,
|
||||
.{arg.sql},
|
||||
));
|
||||
fields[index] = arg;
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
switch (@typeInfo(@TypeOf(arg))) {
|
||||
.enum_literal, .@"enum" => {
|
||||
fields[index] = primaryColumn(Table, @tagName(arg));
|
||||
index += 1;
|
||||
},
|
||||
.@"struct" => {
|
||||
const count = nestedColumns(Table, relations, arg, undefined, true);
|
||||
var buf: [count]Column = undefined;
|
||||
const nested = nestedColumns(Table, relations, arg, &buf, false);
|
||||
@memcpy(fields[index .. index + count], nested);
|
||||
index += count;
|
||||
},
|
||||
.type => {
|
||||
if (@hasField(arg, "__jetquery_function")) {
|
||||
const function = (arg{}).__jetquery_function;
|
||||
var column = primaryColumn(Table, function.column_name);
|
||||
column.function = function.context;
|
||||
column.alias = function.alias;
|
||||
fields[index] = column;
|
||||
index += 1;
|
||||
} else {
|
||||
@compileError("Unexpected type in columns: `" ++ @typeName(arg) ++ "`");
|
||||
}
|
||||
},
|
||||
else => |tag| {
|
||||
@compileError(
|
||||
"Expected [enum, enum_literal, struct] column arguments, found: `" ++ @tagName(tag) ++ "`",
|
||||
);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
}
|
||||
|
||||
fn sizeOf(
|
||||
Table: type,
|
||||
comptime relations: []const type,
|
||||
comptime maybe_args: anytype,
|
||||
) usize {
|
||||
comptime {
|
||||
const args = if (@TypeOf(maybe_args) == @TypeOf(null)) return 0 else maybe_args;
|
||||
|
||||
if (args.len == 0) return Table.columns().len;
|
||||
|
||||
var size: usize = 0;
|
||||
for (args) |arg| {
|
||||
if (@TypeOf(arg) == sql.Function) {
|
||||
_ = primaryColumn(Table, arg.column_name); // validate column
|
||||
size += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (@TypeOf(arg) == Column) {
|
||||
size += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (@typeInfo(@TypeOf(arg))) {
|
||||
.enum_literal, .@"enum" => {
|
||||
_ = primaryColumn(Table, @tagName(arg)); // validate column
|
||||
size += 1;
|
||||
},
|
||||
.pointer => {
|
||||
_ = primaryColumn(Table, arg); // validate column
|
||||
size += 1;
|
||||
},
|
||||
.@"struct" => {
|
||||
size += nestedColumns(Table, relations, arg, undefined, true);
|
||||
},
|
||||
.type => {
|
||||
if (@hasField(arg, "__jetquery_function")) {
|
||||
size += 1;
|
||||
} else {
|
||||
@compileError(
|
||||
"Unsupported type in column arguments: `" ++ @typeName(arg) ++ "`",
|
||||
);
|
||||
}
|
||||
},
|
||||
else => |tag| {
|
||||
@compileError(
|
||||
"Expected [enum, enum_literal, []const u8, struct] column arguments, found: `" ++ @tagName(tag) ++ "`",
|
||||
);
|
||||
},
|
||||
}
|
||||
}
|
||||
return size;
|
||||
}
|
||||
}
|
||||
|
||||
fn primaryColumn(
|
||||
Table: type,
|
||||
comptime name: []const u8,
|
||||
) Column {
|
||||
comptime {
|
||||
for (Table.columns()) |column| {
|
||||
if (std.mem.eql(u8, column.name, name)) return .{
|
||||
.table = Table,
|
||||
.name = name,
|
||||
.type = column.type,
|
||||
};
|
||||
}
|
||||
@compileError(std.fmt.comptimePrint(
|
||||
"Failed matching column `{s}` in Schema for `{s}`.",
|
||||
.{ name, Table.name },
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
fn nestedColumns(
|
||||
Table: type,
|
||||
relations: []const type,
|
||||
comptime arg: anytype,
|
||||
buf: []Column,
|
||||
comptime count: bool,
|
||||
) if (count) usize else []const Column {
|
||||
var index: usize = 0;
|
||||
|
||||
for (std.meta.fields(@TypeOf(arg))) |field| {
|
||||
for (relations) |Relation| {
|
||||
if (std.mem.eql(u8, field.name, Relation.relation_name)) {
|
||||
for (@field(arg, field.name)) |nested_arg| {
|
||||
for (Relation.Source.columns()) |column| {
|
||||
if (std.mem.eql(u8, column.name, @tagName(nested_arg))) {
|
||||
if (!count) {
|
||||
buf[index] = .{
|
||||
.name = @tagName(nested_arg),
|
||||
.table = Relation.Source,
|
||||
.type = column.type,
|
||||
};
|
||||
}
|
||||
index += 1;
|
||||
break;
|
||||
}
|
||||
} else @compileError(std.fmt.comptimePrint(
|
||||
"Failed matching column `{s}.{s}` .",
|
||||
.{ field.name, @tagName(nested_arg) },
|
||||
));
|
||||
}
|
||||
break;
|
||||
}
|
||||
} else @compileError(std.fmt.comptimePrint(
|
||||
"Failed matching relation `{s}` for table `{s}`.",
|
||||
.{ field.name, Table.name },
|
||||
));
|
||||
}
|
||||
|
||||
return if (count) index else buf[0..];
|
||||
}
|
||||
36
src/jetquery/debug.zig
Normal file
36
src/jetquery/debug.zig
Normal file
@ -0,0 +1,36 @@
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
|
||||
pub const CallerInfo = struct {
|
||||
debug_info: *std.debug.SelfInfo,
|
||||
file_name: []const u8,
|
||||
line_number: u64,
|
||||
|
||||
pub fn deinit(self: CallerInfo) void {
|
||||
self.debug_info.allocator.free(self.file_name);
|
||||
}
|
||||
};
|
||||
|
||||
pub fn getCallerInfo(address: usize) !?CallerInfo {
|
||||
if (builtin.mode != .Debug) return null;
|
||||
|
||||
const debug_info = try std.debug.getSelfDebugInfo();
|
||||
const module = debug_info.getModuleForAddress(address) catch |err| switch (err) {
|
||||
error.MissingDebugInfo, error.InvalidDebugInfo => return null,
|
||||
else => return err,
|
||||
};
|
||||
|
||||
const symbol_info = module.getSymbolAtAddress(debug_info.allocator, address) catch |err| switch (err) {
|
||||
error.MissingDebugInfo, error.InvalidDebugInfo => return null,
|
||||
else => return err,
|
||||
};
|
||||
|
||||
return if (symbol_info.source_location) |source_location|
|
||||
.{
|
||||
.debug_info = debug_info,
|
||||
.file_name = source_location.file_name,
|
||||
.line_number = source_location.line,
|
||||
}
|
||||
else
|
||||
null;
|
||||
}
|
||||
2
src/jetquery/default_column_names.zig
Normal file
2
src/jetquery/default_column_names.zig
Normal file
@ -0,0 +1,2 @@
|
||||
pub const created_at = "created_at";
|
||||
pub const updated_at = "updated_at";
|
||||
65
src/jetquery/events.zig
Normal file
65
src/jetquery/events.zig
Normal file
@ -0,0 +1,65 @@
|
||||
const std = @import("std");
|
||||
|
||||
const jetquery = @import("../jetquery.zig");
|
||||
|
||||
/// An event triggered by executing a query. When creating a `Repo`, use the option
|
||||
/// `eventCallback` to specify a function that will receive an `Event` for each query execution.
|
||||
/// Otherwise, `defaultCallback` will be invoked instead.
|
||||
pub const Event = struct {
|
||||
// TODO: Make this a union for failed/successful queries etc.
|
||||
const Error = struct {
|
||||
message: []const u8,
|
||||
err: anyerror,
|
||||
};
|
||||
|
||||
context: jetquery.Context = .query,
|
||||
level: enum { DEBUG, INFO, WARN, ERROR } = .INFO,
|
||||
message: ?[]const u8 = null,
|
||||
sql: ?[]const u8 = null,
|
||||
status: enum { success, fail } = .success,
|
||||
err: ?Error = null,
|
||||
caller_info: ?jetquery.debug.CallerInfo = null,
|
||||
duration: ?i64 = null,
|
||||
};
|
||||
|
||||
pub fn defaultCallback(event: Event) !void {
|
||||
if (event.caller_info) |info| {
|
||||
if (event.context == .query) {
|
||||
const allocator = info.debug_info.allocator;
|
||||
|
||||
const cwd = try std.fs.cwd().realpathAlloc(allocator, ".");
|
||||
defer allocator.free(cwd);
|
||||
|
||||
const relative = try std.fs.path.relative(allocator, cwd, info.file_name);
|
||||
defer allocator.free(relative);
|
||||
|
||||
std.debug.print("[{s}:{}] ", .{ relative, info.line_number });
|
||||
}
|
||||
}
|
||||
|
||||
if (event.err) |err| {
|
||||
std.debug.print(
|
||||
\\
|
||||
\\/
|
||||
\\| Query:
|
||||
\\| {s}
|
||||
\\| Error:
|
||||
\\| {s}: {s}
|
||||
\\\
|
||||
\\
|
||||
, .{ event.sql orelse "", @errorName(err.err), err.message });
|
||||
} else {
|
||||
var buf: [32]u8 = undefined;
|
||||
const formatted_duration = if (event.duration) |duration|
|
||||
try std.fmt.bufPrint(&buf, " [{}]", .{std.fmt.fmtDurationSigned(duration)})
|
||||
else
|
||||
"";
|
||||
std.debug.print("{s}{s}{s}{s}{s}", .{
|
||||
event.message orelse "",
|
||||
if (event.message) |_| "\n" else "",
|
||||
event.sql orelse "",
|
||||
formatted_duration,
|
||||
if (event.sql) |_| "\n" else "",
|
||||
});
|
||||
}
|
||||
}
|
||||
173
src/jetquery/fields.zig
Normal file
173
src/jetquery/fields.zig
Normal file
@ -0,0 +1,173 @@
|
||||
const std = @import("std");
|
||||
|
||||
const jetcommon = @import("jetcommon");
|
||||
|
||||
const Where = @import("sql/Where.zig");
|
||||
|
||||
pub const FieldContext = enum { where, update, insert, limit, offset, order, none };
|
||||
|
||||
pub const FieldInfo = struct {
|
||||
info: std.builtin.Type.StructField,
|
||||
name: []const u8,
|
||||
Table: type,
|
||||
context: FieldContext,
|
||||
};
|
||||
|
||||
pub fn fieldInfos(
|
||||
Adapter: type,
|
||||
Table: type,
|
||||
relations: []const type,
|
||||
T: type,
|
||||
comptime context: FieldContext,
|
||||
) [Where.tree(Adapter, Table, relations, T, context, 0).values_count]FieldInfo {
|
||||
comptime {
|
||||
const tree = Where.tree(Adapter, Table, relations, T, context, 0);
|
||||
var value_fields: [tree.values_count]FieldInfo = undefined;
|
||||
for (std.meta.fields(tree.ValuesTuple), tree.values_fields, 0..) |tuple_field, value_field, index| {
|
||||
value_fields[index] = fieldInfo(tuple_field, value_field.Table, value_field.name, context);
|
||||
}
|
||||
return value_fields;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fieldInfo(
|
||||
comptime field: std.builtin.Type.StructField,
|
||||
Table: type,
|
||||
comptime name: []const u8,
|
||||
comptime context: FieldContext,
|
||||
) FieldInfo {
|
||||
return .{ .info = field, .context = context, .name = name, .Table = Table };
|
||||
}
|
||||
|
||||
pub fn FieldValues(Table: type, relations: []const type, comptime fields: []const FieldInfo) type {
|
||||
_ = Table;
|
||||
_ = relations;
|
||||
var new_fields: [fields.len]std.builtin.Type.StructField = undefined;
|
||||
for (fields, 0..) |field, index| {
|
||||
new_fields[index] = .{
|
||||
.name = std.fmt.comptimePrint("{}", .{index}),
|
||||
.type = field.info.type,
|
||||
.default_value = null,
|
||||
.is_comptime = false,
|
||||
.alignment = @alignOf(field.info.type),
|
||||
};
|
||||
}
|
||||
return @Type(.{
|
||||
.@"struct" = .{
|
||||
.layout = .auto,
|
||||
.fields = &new_fields,
|
||||
.decls = &.{},
|
||||
.is_tuple = true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
pub fn ColumnType(Table: type, comptime field_info: FieldInfo) type {
|
||||
switch (field_info.context) {
|
||||
.limit, .offset => return usize,
|
||||
.where, .update, .insert, .order, .none => {},
|
||||
}
|
||||
|
||||
if (comptime @hasField(Table.Definition, field_info.name)) {
|
||||
const FT = fieldType(Table.Definition, field_info.name);
|
||||
if (FT == jetcommon.types.DateTime) return i64 else return FT;
|
||||
} else {
|
||||
// We only arrive here when we process triplets, e.g.
|
||||
// `.{ .foo, .lt_eql, 100 }`
|
||||
// But we coerce to the other side of the triplet and only use this type as a fallback in
|
||||
// the specific case that two values (i.e. not a column or SQL function) are used on both
|
||||
// sides of the triplet, e.g.:
|
||||
// `.{ 1, .lt, 100 }`
|
||||
// Without a know coercion target the only thing we can do here is use the value's type
|
||||
// and assume the database adapter will know what to do with it, otherwise we get a
|
||||
// compile error and the user has to do an explicit cast. This is all an edge case of an
|
||||
// edge case.
|
||||
return field_info.info.type;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn structField(comptime name: []const u8, T: type) std.builtin.Type.StructField {
|
||||
comptime {
|
||||
return .{
|
||||
.name = name ++ "",
|
||||
.type = T,
|
||||
.default_value = null,
|
||||
.is_comptime = false,
|
||||
.alignment = @alignOf(T),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
pub fn structFieldDefault(comptime name: []const u8, default: anytype) std.builtin.Type.StructField {
|
||||
comptime {
|
||||
return .{
|
||||
.name = name ++ "",
|
||||
.type = @TypeOf(default),
|
||||
.default_value = &default,
|
||||
.is_comptime = false,
|
||||
.alignment = @alignOf(@TypeOf(default)),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
pub fn structFieldComptime(
|
||||
comptime name: []const u8,
|
||||
comptime default: anytype,
|
||||
) std.builtin.Type.StructField {
|
||||
comptime {
|
||||
return .{
|
||||
.name = name ++ "",
|
||||
.type = @TypeOf(default),
|
||||
.default_value = &default,
|
||||
.is_comptime = true,
|
||||
.alignment = @alignOf(@TypeOf(default)),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
pub fn structType(comptime fields: []const std.builtin.Type.StructField) type {
|
||||
return @Type(.{
|
||||
.@"struct" = .{
|
||||
.layout = .auto,
|
||||
.fields = fields,
|
||||
.decls = &.{},
|
||||
.is_tuple = false,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
pub fn fieldType(T: type, comptime name: []const u8) type {
|
||||
const tag = std.enums.nameCast(std.meta.FieldEnum(T), name);
|
||||
const FT = std.meta.fieldInfo(T, tag).type;
|
||||
return switch (@typeInfo(FT)) {
|
||||
.optional => |optional| optional.child,
|
||||
else => FT,
|
||||
};
|
||||
}
|
||||
|
||||
// We need to ensure that we don't store a comptime value otherwise our
|
||||
// entire data structure needs to be comptime-known. This only occurs when a
|
||||
// user does one of the following:
|
||||
//
|
||||
// Triplet where both sides are values:
|
||||
// `.where(.{ 1, .eql, 1 })`
|
||||
// SQL string with mixed runtime and comp-time args:
|
||||
// `.where(.{ "foo = ?", .{ 1, a_runtime_value } })`
|
||||
//
|
||||
// Otherwise we have a target type to coerce to, either from the other side of the triplet, or
|
||||
// whatever the column type is defined in the schema for column values.
|
||||
pub fn ComptimeErasedType(T: type) type {
|
||||
return switch (@typeInfo(T)) {
|
||||
.comptime_int => isize,
|
||||
.comptime_float => f64,
|
||||
else => T,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn ComptimeErasedStructField(field: std.builtin.Type.StructField) std.builtin.Type.StructField {
|
||||
var modified = field;
|
||||
modified.type = ComptimeErasedType(field.type);
|
||||
modified.is_comptime = false;
|
||||
modified.alignment = @alignOf(modified.type);
|
||||
return modified;
|
||||
}
|
||||
380
src/jetquery/reflection/Reflect.zig
Normal file
380
src/jetquery/reflection/Reflect.zig
Normal file
@ -0,0 +1,380 @@
|
||||
const std = @import("std");
|
||||
|
||||
const jetquery = @import("jetquery");
|
||||
const jetcommon = @import("jetcommon");
|
||||
|
||||
const util = @import("util.zig");
|
||||
|
||||
pub const ReflectOptions = struct {
|
||||
header: []const u8 = "",
|
||||
import_jetquery: []const u8 =
|
||||
\\@import("jetquery")
|
||||
,
|
||||
};
|
||||
|
||||
pub fn Reflect(adapter_name: jetquery.adapters.Name, Schema: type) type {
|
||||
return struct {
|
||||
const Self = @This();
|
||||
const AdaptedRepo = jetquery.Repo(adapter_name, Schema);
|
||||
allocator: std.mem.Allocator,
|
||||
repo: *AdaptedRepo,
|
||||
options: ReflectOptions,
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator, repo: *AdaptedRepo, options: ReflectOptions) Self {
|
||||
return .{ .repo = repo, .allocator = allocator, .options = options };
|
||||
}
|
||||
|
||||
pub fn generateSchema(self: Self) ![]const u8 {
|
||||
var arena = std.heap.ArenaAllocator.init(self.allocator);
|
||||
defer arena.deinit();
|
||||
|
||||
const allocator = arena.allocator();
|
||||
|
||||
var buf = std.ArrayList(u8).init(allocator);
|
||||
defer buf.deinit();
|
||||
|
||||
const writer = buf.writer();
|
||||
|
||||
try writer.print(
|
||||
\\{s}
|
||||
\\const jetquery = {s};
|
||||
\\
|
||||
, .{ self.options.header, self.options.import_jetquery });
|
||||
|
||||
const reflection = try self.repo.adapter.reflect(allocator, self.repo);
|
||||
const map = try reflection.tableMap(allocator);
|
||||
var written = std.BufSet.init(allocator);
|
||||
|
||||
// Write tables already defined in the schema first ...
|
||||
inline for (comptime std.meta.declarations(Schema)) |decl| {
|
||||
if (map.get(@field(Schema, decl.name).name)) |table| {
|
||||
try writeModel(allocator, Schema, reflection, table, writer);
|
||||
try written.insert(table.name);
|
||||
}
|
||||
}
|
||||
|
||||
// ... then write any remaining tables to preserve schema order if edited by user.
|
||||
for (reflection.tables) |table| {
|
||||
if (written.contains(table.name)) continue;
|
||||
try writeModel(allocator, Schema, reflection, table, writer);
|
||||
}
|
||||
|
||||
return try self.allocator.dupe(u8, try jetcommon.fmt.zig(
|
||||
allocator,
|
||||
buf.items,
|
||||
"Found errors in generated schema.",
|
||||
));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fn writeModel(
|
||||
allocator: std.mem.Allocator,
|
||||
comptime schema: type,
|
||||
reflection: jetquery.Reflection,
|
||||
table: jetquery.Reflection.TableInfo,
|
||||
writer: anytype,
|
||||
) !void {
|
||||
const model_name = try translateTableName(allocator, schema, table.name);
|
||||
try writer.print(
|
||||
\\
|
||||
\\pub const {s} = jetquery.Model(
|
||||
\\@This(),
|
||||
\\"{s}",
|
||||
\\struct {{
|
||||
\\
|
||||
, .{ model_name, try util.zigEscape(allocator, .string, table.name) });
|
||||
|
||||
for (reflection.columns) |column| {
|
||||
if (!std.mem.eql(u8, column.table, table.name)) continue;
|
||||
try writer.print(
|
||||
\\{s}: {s},
|
||||
\\
|
||||
,
|
||||
.{ try util.zigEscape(allocator, .id, column.name), column.zigType() },
|
||||
);
|
||||
}
|
||||
|
||||
try writer.print(
|
||||
\\}},
|
||||
\\{s}
|
||||
\\);
|
||||
\\
|
||||
, .{try stringifyOptions(allocator, schema, table.name)});
|
||||
}
|
||||
|
||||
fn stringifyOptions(
|
||||
allocator: std.mem.Allocator,
|
||||
comptime schema: type,
|
||||
table_name: []const u8,
|
||||
) ![]const u8 {
|
||||
inline for (comptime std.meta.declarations(schema)) |decl| {
|
||||
const table = @field(schema, decl.name);
|
||||
if (std.mem.eql(u8, table.name, table_name)) {
|
||||
return try stringifyModelOptions(allocator, table);
|
||||
}
|
||||
}
|
||||
|
||||
return ".{}";
|
||||
}
|
||||
|
||||
fn stringifyModelOptions(allocator: std.mem.Allocator, model: type) ![]const u8 {
|
||||
var buf = std.ArrayList(u8).init(allocator);
|
||||
const writer = buf.writer();
|
||||
|
||||
if (comptime hasDefaultOptions(model)) return ".{}";
|
||||
|
||||
try writer.print(
|
||||
\\.{{
|
||||
\\
|
||||
, .{});
|
||||
|
||||
if (comptime !isDefaultPrimaryKey(model)) {
|
||||
try writer.print(
|
||||
\\.primary_key = "{s}",
|
||||
\\
|
||||
, .{try util.zigEscape(allocator, .string, model.primary_key)});
|
||||
}
|
||||
|
||||
const relation_fields = std.meta.fields(@TypeOf(model.relations));
|
||||
|
||||
if (relation_fields.len > 0) {
|
||||
try writer.print(
|
||||
\\.relations = .{{
|
||||
\\
|
||||
, .{});
|
||||
inline for (relation_fields) |field| {
|
||||
const relation = @field(model.relations, field.name);
|
||||
try writer.print(
|
||||
\\.{s} = {s}(.{s}, {s}),
|
||||
\\
|
||||
, .{
|
||||
try util.zigEscape(allocator, .id, field.name),
|
||||
switch (relation.relation_type) {
|
||||
.belongs_to => "jetquery.belongsTo",
|
||||
.has_many => "jetquery.hasMany",
|
||||
},
|
||||
try util.zigEscape(allocator, .id, relation.relation_model_name),
|
||||
try stringifyRelationOptions(allocator, relation),
|
||||
});
|
||||
}
|
||||
|
||||
try writer.print(
|
||||
\\}},
|
||||
\\
|
||||
, .{});
|
||||
}
|
||||
|
||||
try writer.print(
|
||||
\\}},
|
||||
\\
|
||||
, .{});
|
||||
|
||||
return try buf.toOwnedSlice();
|
||||
}
|
||||
|
||||
fn stringifyRelationOptions(allocator: std.mem.Allocator, comptime relation: type) ![]const u8 {
|
||||
if (comptime relation.options.primary_key == null and
|
||||
relation.options.foreign_key == null) return ".{}";
|
||||
|
||||
var buf = std.ArrayList(u8).init(allocator);
|
||||
const writer = buf.writer();
|
||||
|
||||
try writer.print(".{{", .{});
|
||||
if (comptime relation.options.primary_key != null) {
|
||||
try writer.print(
|
||||
\\.primary_key = "{s}"{s}
|
||||
\\
|
||||
, .{
|
||||
try util.zigEscape(allocator, .string, relation.options.primary_key.?),
|
||||
if (comptime relation.options.foreign_key != null) ", " else "",
|
||||
});
|
||||
}
|
||||
|
||||
if (comptime relation.options.foreign_key != null) {
|
||||
try writer.print(
|
||||
\\.foreign_key = "{s}"
|
||||
\\
|
||||
, .{
|
||||
try util.zigEscape(allocator, .string, relation.options.foreign_key.?),
|
||||
});
|
||||
}
|
||||
try writer.print("}}", .{});
|
||||
|
||||
return try buf.toOwnedSlice();
|
||||
}
|
||||
|
||||
fn hasDefaultOptions(comptime model: type) bool {
|
||||
comptime {
|
||||
if (isDefaultPrimaryKey(model)) return false;
|
||||
if (std.meta.fields(@TypeOf(model.relations)).len > 0) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
fn isDefaultPrimaryKey(comptime model: type) bool {
|
||||
comptime {
|
||||
return std.mem.eql(u8, model.primary_key, "id");
|
||||
}
|
||||
}
|
||||
|
||||
fn isDefaultForeignKey(comptime model: type) bool {
|
||||
comptime {
|
||||
return std.mem.eql(u8, model.foreign_key, "_id");
|
||||
}
|
||||
}
|
||||
|
||||
fn translateTableName(
|
||||
allocator: std.mem.Allocator,
|
||||
comptime schema: type,
|
||||
name: []const u8,
|
||||
) ![]const u8 {
|
||||
// First try finding an existing model in the current schema.
|
||||
inline for (comptime std.meta.declarations(schema)) |decl| {
|
||||
if (std.mem.eql(u8, @field(schema, decl.name).name, name)) return decl.name;
|
||||
}
|
||||
|
||||
// Then try translating the table name into a model name. This is the only time we do any
|
||||
// magic with plural nouns etc. - if the user modifies the default generated value, we use
|
||||
// that value going forward as it will now be in the schema.
|
||||
var it = std.mem.tokenizeScalar(u8, name, '_');
|
||||
var buf = std.ArrayList([]const u8).init(allocator);
|
||||
while (it.next()) |token| {
|
||||
var dup = try allocator.dupe(u8, token);
|
||||
dup[0] = std.ascii.toUpper(dup[0]);
|
||||
try buf.append(dup);
|
||||
}
|
||||
return try util.singularize(allocator, try std.mem.join(allocator, "", buf.items));
|
||||
}
|
||||
|
||||
test "reflect" {
|
||||
var admin_repo = try jetquery.Repo(.postgresql, void).init(
|
||||
std.testing.allocator,
|
||||
.{
|
||||
.adapter = .{
|
||||
.database = "postgres",
|
||||
.username = "postgres",
|
||||
.hostname = "127.0.0.1",
|
||||
.password = "password",
|
||||
.port = 5432,
|
||||
},
|
||||
},
|
||||
);
|
||||
defer admin_repo.deinit();
|
||||
try admin_repo.dropDatabase("reflection_test", .{ .if_exists = true });
|
||||
try admin_repo.createDatabase("reflection_test", .{});
|
||||
|
||||
const Schema = struct {
|
||||
pub const Human = jetquery.Model(
|
||||
@This(),
|
||||
"humans",
|
||||
struct { id: i32, name: []const u8 },
|
||||
.{
|
||||
.relations = .{
|
||||
.cats = jetquery.relation.hasMany(.Cat, .{ .foreign_key = "custom_foreign_key" }),
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
pub const Cat = jetquery.Model(
|
||||
@This(),
|
||||
"cats",
|
||||
struct { id: i32, name: []const u8, human_id: i32, paws: i32 },
|
||||
.{
|
||||
.relations = .{
|
||||
.human = jetquery.relation.belongsTo(.Human, .{}),
|
||||
},
|
||||
.primary_key = "custom_primary_key",
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
var repo = try jetquery.Repo(.postgresql, Schema).init(
|
||||
std.testing.allocator,
|
||||
.{
|
||||
.adapter = .{
|
||||
.database = "reflection_test",
|
||||
.username = "postgres",
|
||||
.hostname = "127.0.0.1",
|
||||
.password = "password",
|
||||
.port = 5432,
|
||||
},
|
||||
},
|
||||
);
|
||||
defer repo.deinit();
|
||||
|
||||
try repo.createTable("cats", &.{
|
||||
jetquery.schema.table.primaryKey("id", .{}),
|
||||
jetquery.schema.table.column("name", .string, .{ .not_null = true }),
|
||||
jetquery.schema.table.column("human_id", .integer, .{}),
|
||||
jetquery.schema.table.timestamps(.{}),
|
||||
}, .{});
|
||||
try repo.createTable("humans", &.{
|
||||
jetquery.schema.table.primaryKey("id", .{}),
|
||||
jetquery.schema.table.column("name", .string, .{ .not_null = true }),
|
||||
jetquery.schema.table.timestamps(.{}),
|
||||
}, .{});
|
||||
try repo.createTable("dogs", &.{
|
||||
jetquery.schema.table.primaryKey("id", .{}),
|
||||
jetquery.schema.table.column("name", .string, .{ .not_null = true }),
|
||||
jetquery.schema.table.column("is_woofy", .boolean, .{}),
|
||||
jetquery.schema.table.column("description", .text, .{}),
|
||||
jetquery.schema.table.column("bark_rating", .float, .{}),
|
||||
jetquery.schema.table.column("food_budget", .decimal, .{}),
|
||||
jetquery.schema.table.timestamps(.{}),
|
||||
}, .{});
|
||||
|
||||
const reflect = Reflect(.postgresql, Schema).init(std.testing.allocator, &repo, .{});
|
||||
const schema = try reflect.generateSchema();
|
||||
defer std.testing.allocator.free(schema);
|
||||
try std.testing.expectEqualStrings(
|
||||
\\const jetquery = @import("jetquery");
|
||||
\\
|
||||
\\pub const Human = jetquery.Model(
|
||||
\\ @This(),
|
||||
\\ "humans",
|
||||
\\ struct {
|
||||
\\ id: i32,
|
||||
\\ name: []const u8,
|
||||
\\ created_at: jetquery.DateTime,
|
||||
\\ updated_at: jetquery.DateTime,
|
||||
\\ },
|
||||
\\ .{
|
||||
\\ .relations = .{
|
||||
\\ .cats = jetquery.hasMany(.Cat, .{ .foreign_key = "custom_foreign_key" }),
|
||||
\\ },
|
||||
\\ },
|
||||
\\);
|
||||
\\
|
||||
\\pub const Cat = jetquery.Model(
|
||||
\\ @This(),
|
||||
\\ "cats",
|
||||
\\ struct {
|
||||
\\ id: i32,
|
||||
\\ name: []const u8,
|
||||
\\ human_id: ?i32,
|
||||
\\ created_at: jetquery.DateTime,
|
||||
\\ updated_at: jetquery.DateTime,
|
||||
\\ },
|
||||
\\ .{
|
||||
\\ .primary_key = "custom_primary_key",
|
||||
\\ .relations = .{
|
||||
\\ .human = jetquery.belongsTo(.Human, .{}),
|
||||
\\ },
|
||||
\\ },
|
||||
\\);
|
||||
\\
|
||||
\\pub const Dog = jetquery.Model(@This(), "dogs", struct {
|
||||
\\ id: i32,
|
||||
\\ name: []const u8,
|
||||
\\ is_woofy: ?bool,
|
||||
\\ description: ?[]const u8,
|
||||
\\ bark_rating: ?f64,
|
||||
\\ food_budget: ?[]const u8,
|
||||
\\ created_at: jetquery.DateTime,
|
||||
\\ updated_at: jetquery.DateTime,
|
||||
\\}, .{});
|
||||
\\
|
||||
, schema);
|
||||
}
|
||||
44
src/jetquery/reflection/Reflection.zig
Normal file
44
src/jetquery/reflection/Reflection.zig
Normal file
@ -0,0 +1,44 @@
|
||||
const std = @import("std");
|
||||
|
||||
const jetquery = @import("../../jetquery.zig");
|
||||
|
||||
pub const TableInfo = struct {
|
||||
name: []const u8,
|
||||
};
|
||||
|
||||
pub const ColumnInfo = struct {
|
||||
name: []const u8,
|
||||
table: []const u8,
|
||||
type: jetquery.schema.Column.Type,
|
||||
null: bool,
|
||||
|
||||
pub fn zigType(self: ColumnInfo) []const u8 {
|
||||
return switch (self.type) {
|
||||
.string, .text => if (self.null) "?[]const u8" else "[]const u8",
|
||||
.integer => if (self.null) "?i32" else "i32",
|
||||
.float => if (self.null) "?f64" else "f64",
|
||||
// TODO: Maybe create a Decimal type in jetcommon to wrap pg.Numeric (etc.).
|
||||
// until there's a more standardized Zig decimal type.
|
||||
.decimal => if (self.null) "?[]const u8" else "[]const u8",
|
||||
.boolean => if (self.null) "?bool" else "bool",
|
||||
.datetime => if (self.null) "?jetquery.DateTime" else "jetquery.DateTime",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
allocator: std.mem.Allocator,
|
||||
tables: []const TableInfo,
|
||||
columns: []const ColumnInfo,
|
||||
|
||||
const Reflection = @This();
|
||||
|
||||
pub fn deinit(self: Reflection) void {
|
||||
self.allocator.free(self.tables);
|
||||
self.allocator.free(self.columns);
|
||||
}
|
||||
|
||||
pub fn tableMap(self: Reflection, allocator: std.mem.Allocator) !std.StringHashMap(TableInfo) {
|
||||
var map = std.StringHashMap(TableInfo).init(allocator);
|
||||
for (self.tables) |table| try map.put(table.name, table);
|
||||
return map;
|
||||
}
|
||||
58
src/jetquery/reflection/util.zig
Normal file
58
src/jetquery/reflection/util.zig
Normal file
@ -0,0 +1,58 @@
|
||||
const std = @import("std");
|
||||
|
||||
/// Very basic noun plural->singular conversion. This is used for translating database tables
|
||||
/// into model names. If the user modifies the model name in the generated schema then any
|
||||
/// subsequent schema re-generations will use the value defined by the user. For this reason we
|
||||
/// do not attempt to do anything particularly clever (or deal with other languages, etc.).
|
||||
pub fn singularize(allocator: std.mem.Allocator, input: []const u8) ![]const u8 {
|
||||
const es_endings = [_][]const u8{ "oes", "ses", "xes", "zes", "ches", "shes" };
|
||||
|
||||
for (es_endings) |ending| {
|
||||
if (std.mem.endsWith(u8, input, ending)) {
|
||||
return try allocator.dupe(u8, input[0 .. input.len - 2]);
|
||||
}
|
||||
}
|
||||
|
||||
if (std.mem.endsWith(u8, input, "ies")) {
|
||||
const base = input[0 .. input.len - 3];
|
||||
return try std.mem.concat(allocator, u8, &.{ base, "y" });
|
||||
}
|
||||
|
||||
if (std.mem.endsWith(u8, input, "s")) {
|
||||
return try allocator.dupe(u8, input[0 .. input.len - 1]);
|
||||
}
|
||||
|
||||
return try allocator.dupe(u8, input);
|
||||
}
|
||||
|
||||
test "-s" {
|
||||
const singular = try singularize(std.testing.allocator, "cats");
|
||||
defer std.testing.allocator.free(singular);
|
||||
try std.testing.expectEqualStrings("cat", singular);
|
||||
}
|
||||
test "-ies" {
|
||||
const singular = try singularize(std.testing.allocator, "festivities");
|
||||
defer std.testing.allocator.free(singular);
|
||||
try std.testing.expectEqualStrings("festivity", singular);
|
||||
}
|
||||
test "-shes" {
|
||||
const singular = try singularize(std.testing.allocator, "bushes");
|
||||
defer std.testing.allocator.free(singular);
|
||||
try std.testing.expectEqualStrings("bush", singular);
|
||||
}
|
||||
|
||||
pub fn zigEscape(
|
||||
allocator: std.mem.Allocator,
|
||||
comptime context: enum { id, string },
|
||||
input: []const u8,
|
||||
) ![]const u8 {
|
||||
var buf = std.ArrayList(u8).init(allocator);
|
||||
const writer = buf.writer();
|
||||
const formatter = switch (context) {
|
||||
.id => std.zig.fmtId(input),
|
||||
.string => std.zig.fmtEscapes(input),
|
||||
};
|
||||
|
||||
try writer.print("{}", .{formatter});
|
||||
return try buf.toOwnedSlice();
|
||||
}
|
||||
149
src/jetquery/relation.zig
Normal file
149
src/jetquery/relation.zig
Normal file
@ -0,0 +1,149 @@
|
||||
const std = @import("std");
|
||||
|
||||
const jetquery = @import("../jetquery.zig");
|
||||
|
||||
pub fn RelationsEnum(Table: type) type {
|
||||
return std.meta.FieldEnum(@TypeOf(Table.relations));
|
||||
}
|
||||
|
||||
pub fn ColumnsEnum(Schema: type, Table: type, comptime name: RelationsEnum(Table)) type {
|
||||
comptime {
|
||||
const relation = @field(Table.relations, @tagName(name));
|
||||
const source = std.enums.nameCast(std.meta.DeclEnum(Schema), relation.relation_model_name);
|
||||
const Source = @field(Schema, @tagName(source));
|
||||
return std.meta.FieldEnum(Source.Definition);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn RelationTable(Schema: type, Table: type, comptime name: RelationsEnum(Table)) type {
|
||||
const relation = @field(Table.relations, @tagName(name));
|
||||
const source = std.enums.nameCast(std.meta.DeclEnum(Schema), relation.relation_model_name);
|
||||
|
||||
return @field(Schema, @tagName(source));
|
||||
}
|
||||
|
||||
pub const JoinContext = enum { inner, outer, include };
|
||||
|
||||
pub fn concatRelations(
|
||||
relations: []const type,
|
||||
Schema: type,
|
||||
Table: type,
|
||||
comptime name: RelationsEnum(Table),
|
||||
comptime relation_options: RelationOptions,
|
||||
comptime join_context: JoinContext,
|
||||
) *const [1 + relations.len]type {
|
||||
comptime {
|
||||
var types: [1 + relations.len]type = undefined;
|
||||
for (relations, 0..) |relation, index| types[index] = relation;
|
||||
types[relations.len] = Relation(
|
||||
Schema,
|
||||
Table,
|
||||
name,
|
||||
relation_options,
|
||||
join_context,
|
||||
);
|
||||
const final = types;
|
||||
return &final;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn Relation(
|
||||
Schema: type,
|
||||
Table: type,
|
||||
comptime name: RelationsEnum(Table),
|
||||
comptime relation_options: RelationOptions,
|
||||
comptime join_context: JoinContext,
|
||||
) type {
|
||||
comptime {
|
||||
const relation = @field(Table.relations, @tagName(name));
|
||||
return struct {
|
||||
pub const context = join_context;
|
||||
pub const Source = RelationTable(Schema, Table, name);
|
||||
pub const relation_type = relation.relation_type;
|
||||
pub const options = relation.options;
|
||||
pub const relation_name = @tagName(name);
|
||||
pub const select_columns = relation_options.select;
|
||||
pub const limit = relation_options.limit;
|
||||
pub const order_by = relation_options.order_by;
|
||||
pub const primary_key = options.primary_key orelse "id";
|
||||
pub const foreign_key: ?[]const u8 = options.foreign_key orelse switch (relation_type) {
|
||||
.belongs_to => relation_name ++ "_id",
|
||||
.has_many => null, // We have to infer this from the source table later.
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
pub const RelationOptions = struct {
|
||||
select: []const jetquery.columns.Column = &.{},
|
||||
limit: ?u64 = null,
|
||||
order_by: ?[]const jetquery.sql.OrderClause = &.{},
|
||||
};
|
||||
|
||||
pub fn translateRelationOptions(
|
||||
Schema: type,
|
||||
Model: type,
|
||||
comptime name: RelationsEnum(Model),
|
||||
comptime options: anytype,
|
||||
) RelationOptions {
|
||||
var translated: RelationOptions = .{};
|
||||
|
||||
const Source = RelationTable(Schema, Model, name);
|
||||
|
||||
translated.select = &jetquery.columns.translate(
|
||||
Source,
|
||||
&.{},
|
||||
if (@hasField(@TypeOf(options), "select")) options.select else .{},
|
||||
);
|
||||
|
||||
translated.limit = if (@hasField(@TypeOf(options), "limit"))
|
||||
options.limit
|
||||
else
|
||||
null;
|
||||
|
||||
translated.order_by = if (@hasField(@TypeOf(options), "order_by"))
|
||||
&jetquery.sql.translateOrderBy(Source, &.{}, options.order_by)
|
||||
else
|
||||
null;
|
||||
|
||||
return translated;
|
||||
}
|
||||
|
||||
pub const RelationType = enum { belongs_to, has_many };
|
||||
|
||||
pub const BelongsToOptions = struct {
|
||||
primary_key: ?[]const u8 = null,
|
||||
foreign_key: ?[]const u8 = null,
|
||||
};
|
||||
|
||||
pub fn belongsTo(comptime model_name: anytype, comptime belongs_to_options: BelongsToOptions) type {
|
||||
return struct {
|
||||
pub const relation_model_name = @tagName(model_name);
|
||||
pub const relation_type: RelationType = .belongs_to;
|
||||
pub const options = belongs_to_options;
|
||||
};
|
||||
}
|
||||
|
||||
pub const HasManyOptions = struct {
|
||||
primary_key: ?[]const u8 = null,
|
||||
foreign_key: ?[]const u8 = null,
|
||||
};
|
||||
|
||||
pub fn hasMany(comptime model_name: anytype, comptime has_many_options: HasManyOptions) type {
|
||||
return struct {
|
||||
pub const relation_model_name = @tagName(model_name);
|
||||
pub const relation_type: RelationType = .has_many;
|
||||
pub const options = has_many_options;
|
||||
};
|
||||
}
|
||||
|
||||
fn detectLimit(options: anytype, relation_type: RelationType) ?u64 {
|
||||
if (@hasField(@TypeOf(options), "limit")) {
|
||||
if (relation_type != .has_many) {
|
||||
@compileError(
|
||||
"`limit` on `include` only supported for `has_many` relations, found: `" ++
|
||||
@tagName(relation_type) ++ "`",
|
||||
);
|
||||
} else return @intCast(options.limit);
|
||||
} else return null;
|
||||
}
|
||||
2
src/jetquery/schema.zig
Normal file
2
src/jetquery/schema.zig
Normal file
@ -0,0 +1,2 @@
|
||||
pub const table = @import("schema/table.zig");
|
||||
pub const Column = @import("schema/Column.zig");
|
||||
29
src/jetquery/schema/Column.zig
Normal file
29
src/jetquery/schema/Column.zig
Normal file
@ -0,0 +1,29 @@
|
||||
const table = @import("table.zig");
|
||||
|
||||
/// A database column.
|
||||
pub const Column = @This();
|
||||
|
||||
name: []const u8,
|
||||
type: Type,
|
||||
options: Options = .{},
|
||||
primary_key: bool = false,
|
||||
timestamps: ?table.TimestampsOptions = null,
|
||||
|
||||
pub const Type = enum { string, integer, float, decimal, boolean, datetime, text };
|
||||
pub const Reference = [2][]const u8;
|
||||
pub const Options = struct {
|
||||
not_null: bool = false,
|
||||
index: bool = false,
|
||||
index_name: ?[]const u8 = null,
|
||||
unique: bool = false,
|
||||
reference: ?Reference = null,
|
||||
length: ?u16 = null,
|
||||
};
|
||||
|
||||
pub fn init(
|
||||
comptime name: []const u8,
|
||||
comptime column_type: Type,
|
||||
comptime options: Options,
|
||||
) Column {
|
||||
return .{ .name = name, .type = column_type, .options = options };
|
||||
}
|
||||
69
src/jetquery/schema/table.zig
Normal file
69
src/jetquery/schema/table.zig
Normal file
@ -0,0 +1,69 @@
|
||||
const std = @import("std");
|
||||
|
||||
const jetquery = @import("../../jetquery.zig");
|
||||
|
||||
const Column = @import("Column.zig");
|
||||
|
||||
pub fn column(
|
||||
name: []const u8,
|
||||
column_type: Column.Type,
|
||||
options: Column.Options,
|
||||
) Column {
|
||||
return .{ .name = name, .type = column_type, .options = options };
|
||||
}
|
||||
|
||||
pub const PrimaryKeyOptions = struct {
|
||||
type: Column.Type = .integer,
|
||||
};
|
||||
|
||||
pub fn primaryKey(name: []const u8, options: PrimaryKeyOptions) Column {
|
||||
return .{
|
||||
.name = name,
|
||||
.type = options.type,
|
||||
.options = .{ .not_null = true },
|
||||
.primary_key = true,
|
||||
};
|
||||
}
|
||||
|
||||
pub const TimestampsOptions = struct {
|
||||
created_at: bool = true,
|
||||
updated_at: bool = true,
|
||||
|
||||
pub fn toSql(
|
||||
self: TimestampsOptions,
|
||||
writer: anytype,
|
||||
adapter: anytype,
|
||||
) !void {
|
||||
const created_at = comptime Column.init(
|
||||
jetquery.default_column_names.created_at,
|
||||
.datetime,
|
||||
.{},
|
||||
);
|
||||
const updated_at = comptime Column.init(
|
||||
jetquery.default_column_names.created_at,
|
||||
.datetime,
|
||||
.{},
|
||||
);
|
||||
if (self.created_at) {
|
||||
try writer.print("{s}{s}{s}", .{
|
||||
adapter.identifier(jetquery.default_column_names.created_at),
|
||||
adapter.columnTypeSql(created_at),
|
||||
adapter.notNullSql(),
|
||||
});
|
||||
}
|
||||
|
||||
if (self.created_at and self.updated_at) try writer.print(", ", .{});
|
||||
|
||||
if (self.updated_at) {
|
||||
try writer.print("{s}{s}{s}", .{
|
||||
adapter.identifier(jetquery.default_column_names.updated_at),
|
||||
adapter.columnTypeSql(updated_at),
|
||||
adapter.notNullSql(),
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
pub fn timestamps(options: TimestampsOptions) Column {
|
||||
return .{ .name = undefined, .type = undefined, .options = .{}, .timestamps = options };
|
||||
}
|
||||
213
src/jetquery/sql.zig
Normal file
213
src/jetquery/sql.zig
Normal file
@ -0,0 +1,213 @@
|
||||
const std = @import("std");
|
||||
|
||||
const jetquery = @import("../jetquery.zig");
|
||||
|
||||
pub const render = @import("sql/render.zig").render;
|
||||
pub const renderUpdateRuntime = @import("sql/render.zig").renderUpdateRuntime;
|
||||
pub const Where = @import("sql/Where.zig");
|
||||
|
||||
pub const FieldState = struct {
|
||||
name: []const u8,
|
||||
modified: bool,
|
||||
};
|
||||
|
||||
pub const QueryContext = enum {
|
||||
select,
|
||||
update,
|
||||
insert,
|
||||
delete,
|
||||
delete_all,
|
||||
count,
|
||||
none,
|
||||
};
|
||||
|
||||
pub const OrderClause = struct {
|
||||
// TODO: Allow ordering on relations.
|
||||
column: jetquery.columns.Column,
|
||||
direction: OrderDirection,
|
||||
};
|
||||
|
||||
pub const OrderDirection = enum { ascending, descending, asc, desc };
|
||||
pub const CountContext = enum { all, distinct };
|
||||
pub const CountColumn = struct {
|
||||
type: CountContext,
|
||||
};
|
||||
|
||||
// Information about a column's position in a `SELECT` query, used to determine how to assign
|
||||
// result values to the `ResultType`. We cannot rely on the column name returned by PostgreSQL as
|
||||
// we only get the name of the column and not the table it came from, so we would otherwise not
|
||||
// be able to match columns to tables in joins. This also means we can skip a few allocs in
|
||||
// pg.zig by not requesting column names.
|
||||
pub const ColumnInfo = struct {
|
||||
index: usize,
|
||||
name: []const u8,
|
||||
type: type,
|
||||
relation: ?type,
|
||||
};
|
||||
|
||||
pub const FunctionContext = enum { min, max, count, avg, sum };
|
||||
pub const Function = struct {
|
||||
context: FunctionContext,
|
||||
column_name: []const u8,
|
||||
alias: []const u8,
|
||||
};
|
||||
|
||||
fn FunctionType(comptime context: FunctionContext, comptime column_tag: anytype) type {
|
||||
if (@typeInfo(@TypeOf(column_tag)) != .enum_literal) {
|
||||
@compileError(std.fmt.comptimePrint(
|
||||
"Expected enum literal as SQL function argument, found: `{s}`",
|
||||
.{@tagName(@typeInfo(@TypeOf(column_tag)))},
|
||||
));
|
||||
}
|
||||
|
||||
return struct {
|
||||
comptime __jetquery_function: Function = .{
|
||||
.context = context,
|
||||
.column_name = @tagName(column_tag),
|
||||
.alias = @tagName(context) ++ "__" ++ @tagName(column_tag),
|
||||
},
|
||||
|
||||
pub fn as(comptime alias: []const u8) type {
|
||||
return struct {
|
||||
comptime __jetquery_function: Function = .{
|
||||
.context = context,
|
||||
.column_name = @tagName(column_tag),
|
||||
.alias = alias,
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub inline fn min(comptime column_tag: anytype) type {
|
||||
return FunctionType(.min, column_tag);
|
||||
}
|
||||
|
||||
pub inline fn max(comptime column_tag: anytype) type {
|
||||
return FunctionType(.max, column_tag);
|
||||
}
|
||||
|
||||
pub inline fn count(comptime column_tag: anytype) type {
|
||||
return FunctionType(.count, column_tag);
|
||||
}
|
||||
|
||||
pub inline fn avg(comptime column_tag: anytype) type {
|
||||
return FunctionType(.avg, column_tag);
|
||||
}
|
||||
|
||||
pub inline fn sum(comptime column_tag: anytype) type {
|
||||
return FunctionType(.sum, column_tag);
|
||||
}
|
||||
|
||||
pub inline fn column(T: type, comptime sql: []const u8) jetquery.columns.Column {
|
||||
return jetquery.columns.Column{
|
||||
.name = undefined,
|
||||
.type = T,
|
||||
.table = undefined,
|
||||
.function = null,
|
||||
.alias = null,
|
||||
.sql = sql,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn translateOrderBy(
|
||||
Table: type,
|
||||
relations: []const type,
|
||||
comptime args: anytype,
|
||||
) [orderBySize(@TypeOf(args))]jetquery.sql.OrderClause {
|
||||
comptime {
|
||||
switch (@typeInfo(@TypeOf(args))) {
|
||||
.enum_literal => return .{.{
|
||||
.column = Table.column(@tagName(args)),
|
||||
.direction = .ascending,
|
||||
}},
|
||||
.@"struct" => {},
|
||||
else => |tag| @compileError(
|
||||
std.fmt.comptimePrint(
|
||||
"Unsupported `orderBy` argument: `{s}`. Expected [enum_literal, struct]",
|
||||
.{@tagName(tag)},
|
||||
),
|
||||
),
|
||||
}
|
||||
var clauses: [orderBySize(@TypeOf(args))]jetquery.sql.OrderClause = undefined;
|
||||
const is_tuple = @typeInfo(@TypeOf(args)).@"struct".is_tuple;
|
||||
const fields = std.meta.fields(@TypeOf(args));
|
||||
|
||||
var index: usize = 0;
|
||||
for (fields, if (is_tuple) args else fields) |field, arg| {
|
||||
if (is_tuple) {
|
||||
// Short-hand (default ascending):
|
||||
// orderBy(.{ .foo, .bar, .baz })
|
||||
clauses[index] = .{
|
||||
.column = Table.column(@tagName(arg)),
|
||||
.direction = .ascending,
|
||||
};
|
||||
index += 1;
|
||||
continue;
|
||||
} else if (@hasField(Table.Definition, field.name)) {
|
||||
// Explicit form:
|
||||
// orderBy(.{ .foo = .ascending })
|
||||
// orderBy(.{ .bar = .descending })
|
||||
clauses[index] = .{
|
||||
.column = Table.column(field.name),
|
||||
.direction = std.enums.nameCast(
|
||||
jetquery.sql.OrderDirection,
|
||||
@tagName(@field(args, field.name)),
|
||||
),
|
||||
};
|
||||
index += 1;
|
||||
continue;
|
||||
} else {
|
||||
// Nested form, ordering by relations fields:
|
||||
// orderBy(.{ .foo = .{ .bar })
|
||||
// orderBy(.{ .foo = .{ .bar = .descending } })
|
||||
relations: for (relations) |relation| {
|
||||
if (std.mem.eql(u8, relation.relation_name, field.name)) {
|
||||
const nested_clauses = translateOrderBy(
|
||||
relation.Source,
|
||||
&.{},
|
||||
@field(args, field.name),
|
||||
);
|
||||
for (nested_clauses) |clause| {
|
||||
clauses[index] = clause;
|
||||
index += 1;
|
||||
}
|
||||
break :relations;
|
||||
}
|
||||
} else {
|
||||
@compileError(
|
||||
std.fmt.comptimePrint(
|
||||
"Unrecognized `orderBy` field `{s}` in current table and active joins/includes.",
|
||||
.{field.name},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return clauses;
|
||||
}
|
||||
}
|
||||
|
||||
fn orderBySize(T: type) usize {
|
||||
const error_message = "Unsupported argument type for `orderBy`: `{s}`. Expected [enum_literal, struct]";
|
||||
|
||||
return switch (@typeInfo(T)) {
|
||||
.enum_literal => 1,
|
||||
.@"struct" => blk: {
|
||||
var size: usize = 0;
|
||||
for (std.meta.fields(T)) |field| {
|
||||
size += switch (@typeInfo(field.type)) {
|
||||
.enum_literal => 1,
|
||||
.@"struct" => orderBySize(field.type),
|
||||
else => |tag| @compileError(
|
||||
std.fmt.comptimePrint(error_message, .{@tagName(tag)}),
|
||||
),
|
||||
};
|
||||
}
|
||||
break :blk size;
|
||||
},
|
||||
else => |tag| @compileError(
|
||||
std.fmt.comptimePrint(error_message, .{@tagName(tag)}),
|
||||
),
|
||||
};
|
||||
}
|
||||
999
src/jetquery/sql/Where.zig
Normal file
999
src/jetquery/sql/Where.zig
Normal file
@ -0,0 +1,999 @@
|
||||
const std = @import("std");
|
||||
|
||||
const fields = @import("../fields.zig");
|
||||
const coercion = @import("../coercion.zig");
|
||||
const columns = @import("../columns.zig");
|
||||
const sql = @import("../sql.zig");
|
||||
|
||||
const Where = @This();
|
||||
|
||||
fn ClauseValues(ValuesTuple: type, ErrorsTuple: type) type {
|
||||
return struct {
|
||||
values: ValuesTuple,
|
||||
errors: ErrorsTuple,
|
||||
};
|
||||
}
|
||||
|
||||
pub const Field = struct {
|
||||
name: []const u8,
|
||||
Table: type,
|
||||
column_type: type,
|
||||
context: fields.FieldContext,
|
||||
index: usize,
|
||||
};
|
||||
|
||||
pub const Tree = struct {
|
||||
Table: type,
|
||||
relations: []const type,
|
||||
root: Node,
|
||||
values_count: usize,
|
||||
values_fields: []const Field,
|
||||
ValuesTuple: type,
|
||||
ErrorsTuple: type,
|
||||
|
||||
const Counter = struct {
|
||||
count: usize,
|
||||
|
||||
const Self = @This();
|
||||
|
||||
pub fn write(self: *Self, bytes: []const u8) !void {
|
||||
self.count += bytes.len;
|
||||
}
|
||||
|
||||
pub fn print(self: *Self, comptime fmt: []const u8, comptime args: anytype) !void {
|
||||
self.write(std.fmt.comptimePrint(fmt, args)) catch unreachable;
|
||||
}
|
||||
};
|
||||
|
||||
pub fn render(comptime self: Tree, Adapter: type) []const u8 {
|
||||
comptime {
|
||||
var counter = Counter{ .count = 0 };
|
||||
self.root.render(Adapter, &counter, 0, null);
|
||||
|
||||
var buf: [counter.count]u8 = undefined;
|
||||
var stream = std.io.fixedBufferStream(&buf);
|
||||
self.root.render(Adapter, stream.writer(), 0, null);
|
||||
|
||||
return stream.getWritten() ++ "";
|
||||
}
|
||||
}
|
||||
|
||||
pub fn values(comptime self: Tree, args: anytype) ClauseValues(self.ValuesTuple, self.ErrorsTuple) {
|
||||
var vals: self.ValuesTuple = undefined;
|
||||
var errors: self.ErrorsTuple = undefined;
|
||||
if (@typeInfo(@TypeOf(args)) != .@"struct") @compileError(
|
||||
"Expected `struct`, found `" ++ @tagName(@typeInfo(@TypeOf(args))) ++ "`",
|
||||
);
|
||||
assignValues(
|
||||
args,
|
||||
self.ValuesTuple,
|
||||
&vals,
|
||||
self.ErrorsTuple,
|
||||
&errors,
|
||||
self.values_fields,
|
||||
0,
|
||||
true,
|
||||
);
|
||||
return .{ .values = vals, .errors = errors };
|
||||
}
|
||||
|
||||
pub fn fields(comptime self: Tree) [self.root.countValues()]Field {
|
||||
return self.root.values_fields(self.Table, self.relations);
|
||||
}
|
||||
|
||||
pub fn countValues(comptime self: Tree) usize {
|
||||
return self.root.countValues();
|
||||
}
|
||||
};
|
||||
|
||||
pub fn tree(
|
||||
Adapter: type,
|
||||
Table: type,
|
||||
relations: []const type,
|
||||
T: type,
|
||||
comptime field_context: fields.FieldContext,
|
||||
comptime first_value_index: usize,
|
||||
) Tree {
|
||||
var index: usize = first_value_index;
|
||||
const root = nodeTree(
|
||||
Adapter,
|
||||
Table,
|
||||
relations,
|
||||
T,
|
||||
T,
|
||||
"root",
|
||||
undefined,
|
||||
&.{},
|
||||
field_context,
|
||||
&index,
|
||||
);
|
||||
|
||||
return .{
|
||||
.root = root,
|
||||
.Table = Table,
|
||||
.relations = relations,
|
||||
.values_count = root.countValues(),
|
||||
.values_fields = &root.values_fields(Table, relations),
|
||||
.ValuesTuple = root.ValuesTuple(),
|
||||
.ErrorsTuple = root.ErrorsTuple(),
|
||||
};
|
||||
}
|
||||
|
||||
pub const Node = union(enum) {
|
||||
pub const Condition = enum { NOT, AND, OR };
|
||||
pub const Value = struct {
|
||||
name: []const u8,
|
||||
type: type,
|
||||
Table: type,
|
||||
field_info: std.builtin.Type.StructField,
|
||||
field_context: fields.FieldContext,
|
||||
index: usize,
|
||||
synthetic: bool = false,
|
||||
|
||||
pub fn ColumnType(self: Value) type {
|
||||
const T = fields.ColumnType(self.Table, fields.fieldInfo(
|
||||
self.field_info,
|
||||
self.Table,
|
||||
self.name,
|
||||
self.field_context,
|
||||
));
|
||||
return if (self.isArray()) []const T else T;
|
||||
}
|
||||
|
||||
pub fn isArray(self: Value) bool {
|
||||
const T = fields.ColumnType(self.Table, fields.fieldInfo(
|
||||
self.field_info,
|
||||
self.Table,
|
||||
self.name,
|
||||
self.field_context,
|
||||
));
|
||||
|
||||
return switch (@typeInfo(self.type)) {
|
||||
.pointer => |info| if (info.size == .Slice and info.child == T)
|
||||
true
|
||||
else
|
||||
false,
|
||||
else => false,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn isNull(self: Value) bool {
|
||||
return (self.type == @TypeOf(null));
|
||||
}
|
||||
};
|
||||
|
||||
pub const Group = struct {
|
||||
name: []const u8,
|
||||
children: []const Node,
|
||||
};
|
||||
|
||||
pub const Triplet = struct {
|
||||
lhs: Operand,
|
||||
operator: Operator,
|
||||
rhs: Operand,
|
||||
|
||||
pub const Operator = enum {
|
||||
eql,
|
||||
not_eql,
|
||||
lt,
|
||||
lt_eql,
|
||||
gt,
|
||||
gt_eql,
|
||||
like,
|
||||
ilike, // Not supported by all databases
|
||||
};
|
||||
|
||||
pub const Operand = union(enum) {
|
||||
value: Node.Value,
|
||||
column: columns.Column,
|
||||
};
|
||||
};
|
||||
|
||||
pub const SqlString = struct {
|
||||
sql: []const u8,
|
||||
values: []const Node,
|
||||
|
||||
pub fn render(comptime self: SqlString, Adapter: type) []const u8 {
|
||||
var single_quoted = false;
|
||||
var double_quoted = false;
|
||||
var indices: [self.values.len]usize = undefined;
|
||||
var arg_index: usize = 0;
|
||||
|
||||
for (self.sql, 0..) |char, char_index| {
|
||||
switch (char) {
|
||||
'\'' => single_quoted = !single_quoted,
|
||||
'"' => double_quoted = !double_quoted,
|
||||
'?' => if (!double_quoted and !single_quoted) {
|
||||
if (arg_index < self.values.len) {
|
||||
indices[arg_index] = char_index;
|
||||
}
|
||||
arg_index += 1;
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
if (arg_index != self.values.len) {
|
||||
@compileError(std.fmt.comptimePrint(
|
||||
"Expected {} arguments to string clause, found {}. SQL string: `{s}`",
|
||||
.{ self.values.len, arg_index, self.sql },
|
||||
));
|
||||
}
|
||||
|
||||
var size: usize = 0;
|
||||
var cursor: usize = 0;
|
||||
for (indices, self.values) |index, node| {
|
||||
const chunk = self.sql[cursor..index];
|
||||
const output = chunk ++ Adapter.paramSql(node.value.index);
|
||||
cursor += chunk.len + 1;
|
||||
size += output.len;
|
||||
}
|
||||
|
||||
var buf: [size]u8 = undefined;
|
||||
var input_cursor: usize = 0;
|
||||
var output_cursor: usize = 0;
|
||||
|
||||
for (indices, self.values) |index, node| {
|
||||
const chunk = self.sql[input_cursor..index];
|
||||
const output = chunk ++ Adapter.paramSql(node.value.index);
|
||||
@memcpy(buf[output_cursor .. output_cursor + output.len], output);
|
||||
input_cursor += chunk.len + 1;
|
||||
output_cursor += output.len;
|
||||
}
|
||||
const final = buf;
|
||||
return &final;
|
||||
}
|
||||
};
|
||||
|
||||
condition: Condition,
|
||||
value: Value,
|
||||
group: Group,
|
||||
triplet: Triplet,
|
||||
sql_string: SqlString,
|
||||
|
||||
pub fn render(
|
||||
self: Node,
|
||||
Adapter: type,
|
||||
comptime writer: anytype,
|
||||
comptime depth: usize,
|
||||
comptime prev: ?Node,
|
||||
) void {
|
||||
switch (self) {
|
||||
.condition => |capture| {
|
||||
const operator = switch (capture) {
|
||||
.NOT => if (prev == null) "NOT" else "AND NOT",
|
||||
else => |tag| @tagName(tag),
|
||||
};
|
||||
writer.print(" {s} ", .{operator}) catch unreachable;
|
||||
},
|
||||
.value => |value| {
|
||||
const is_sequence = if (prev) |capture| capture == .value else false;
|
||||
const prefix = if (is_sequence) " AND " else "";
|
||||
if (value.type == @TypeOf(null)) {
|
||||
writer.print("{s}{s}.{s} IS NULL", .{
|
||||
prefix,
|
||||
Adapter.identifier(value.Table.name),
|
||||
Adapter.identifier(value.name),
|
||||
}) catch unreachable;
|
||||
} else {
|
||||
writer.print("{s}{s}.{s} = {s}", .{
|
||||
prefix,
|
||||
Adapter.identifier(value.Table.name),
|
||||
Adapter.identifier(value.name),
|
||||
if (value.isArray())
|
||||
// XXX: This is PostgreSQL-specific - one day we'll need to figure
|
||||
// out how to generate SQL for unknown (at comptime) array length.
|
||||
// MySQL has `ANY` but it expects a subquery so maybe we'll need a
|
||||
// temporary table or something equally horrible. SQLite doesn't have
|
||||
// `ANY` at all so we may end up having to generate some parts of the
|
||||
// SQL at runtime. :(
|
||||
Adapter.anyParamSql(value.index)
|
||||
else
|
||||
Adapter.paramSql(value.index),
|
||||
}) catch unreachable;
|
||||
}
|
||||
},
|
||||
.group => |group| {
|
||||
if (group.children.len > 1) writer.print("(", .{}) catch unreachable;
|
||||
var prev_child: ?Node = null;
|
||||
for (group.children) |child| {
|
||||
if (prev_child) |capture| {
|
||||
const is_and = switch (child) {
|
||||
.group, .triplet => switch (capture) {
|
||||
.group, .value, .triplet => true,
|
||||
else => false,
|
||||
},
|
||||
else => false,
|
||||
};
|
||||
if (is_and) {
|
||||
writer.print(" AND ", .{}) catch unreachable;
|
||||
}
|
||||
}
|
||||
child.render(Adapter, writer, depth + 1, prev_child);
|
||||
prev_child = child;
|
||||
}
|
||||
if (group.children.len > 1) writer.print(")", .{}) catch unreachable;
|
||||
},
|
||||
.triplet => |triplet| {
|
||||
switch (triplet.lhs) {
|
||||
.value => |value| {
|
||||
writer.print("{s}", .{Adapter.paramSql(value.index)}) catch unreachable;
|
||||
},
|
||||
.column => |column| {
|
||||
writer.print("{s}", .{Adapter.columnSql(column)}) catch unreachable;
|
||||
},
|
||||
}
|
||||
|
||||
const operator = switch (triplet.operator) {
|
||||
.eql => "=",
|
||||
.not_eql => "<>",
|
||||
.lt => "<",
|
||||
.lt_eql => "<=",
|
||||
.gt => ">",
|
||||
.gt_eql => ">=",
|
||||
.like => "LIKE",
|
||||
.ilike => "ILIKE", // Not supported by all databases
|
||||
};
|
||||
|
||||
writer.print(" {s} ", .{operator}) catch unreachable;
|
||||
|
||||
switch (triplet.rhs) {
|
||||
.value => |value| {
|
||||
writer.print("{s}", .{Adapter.paramSql(value.index)}) catch unreachable;
|
||||
},
|
||||
.column => |column| {
|
||||
writer.print("{s}", .{Adapter.columnSql(column)}) catch unreachable;
|
||||
},
|
||||
}
|
||||
},
|
||||
.sql_string => |sql_string| {
|
||||
_ = writer.write(sql_string.render(Adapter)) catch unreachable;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn ValuesTuple(comptime self: Node) type {
|
||||
const len = self.countValues();
|
||||
var types: [len]type = undefined;
|
||||
var index: usize = 0;
|
||||
|
||||
appendValueType(self, len, &types, &index);
|
||||
return std.meta.Tuple(&types);
|
||||
}
|
||||
|
||||
fn appendValueType(
|
||||
comptime node: Node,
|
||||
comptime len: usize,
|
||||
types: *[len]type,
|
||||
index: *usize,
|
||||
) void {
|
||||
switch (node) {
|
||||
.condition => {},
|
||||
.value => |value| {
|
||||
if (!value.isNull()) {
|
||||
types[index.*] = value.ColumnType();
|
||||
index.* += 1;
|
||||
}
|
||||
},
|
||||
.group => |group| {
|
||||
for (group.children) |child| appendValueType(child, len, types, index);
|
||||
},
|
||||
.triplet => |triplet| {
|
||||
switch (triplet.lhs) {
|
||||
.value => |value| {
|
||||
if (!value.isNull()) {
|
||||
types[index.*] = value.type;
|
||||
index.* += 1;
|
||||
}
|
||||
},
|
||||
.column => {},
|
||||
}
|
||||
switch (triplet.rhs) {
|
||||
.value => |value| {
|
||||
if (!value.isNull()) {
|
||||
types[index.*] = value.type;
|
||||
index.* += 1;
|
||||
}
|
||||
},
|
||||
.column => {},
|
||||
}
|
||||
},
|
||||
.sql_string => |sql_string| {
|
||||
for (sql_string.values) |value_node| {
|
||||
if (@typeInfo(value_node.value.type) == .@"struct") {
|
||||
@compileError(std.fmt.comptimePrint(
|
||||
"Unsupported type in SQL string arguments: `struct`. SQL string: `{s}`",
|
||||
.{sql_string.sql},
|
||||
));
|
||||
}
|
||||
appendValueType(value_node, len, types, index);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn ErrorsTuple(comptime self: Node) type {
|
||||
var types: [self.countValues()]type = undefined;
|
||||
for (0..self.countValues()) |index| {
|
||||
types[index] = ?anyerror;
|
||||
}
|
||||
|
||||
return std.meta.Tuple(&types);
|
||||
}
|
||||
|
||||
fn values_fields(comptime self: Node, Table: type, relations: []const type) [self.countValues()]Field {
|
||||
const len = self.countValues();
|
||||
var fields_array: [len]Field = undefined;
|
||||
var tuple_index: usize = 0;
|
||||
appendField(self, Table, relations, len, &fields_array, &tuple_index);
|
||||
|
||||
return fields_array;
|
||||
}
|
||||
|
||||
fn appendValueField(
|
||||
T: type,
|
||||
comptime value: Node.Value,
|
||||
comptime len: usize,
|
||||
fields_array: *[len]Field,
|
||||
tuple_index: *usize,
|
||||
) void {
|
||||
if (!value.isNull()) {
|
||||
fields_array[tuple_index.*] = .{
|
||||
.Table = value.Table,
|
||||
.name = value.name,
|
||||
.context = value.field_context,
|
||||
.column_type = T,
|
||||
.index = tuple_index.*,
|
||||
};
|
||||
tuple_index.* += 1;
|
||||
}
|
||||
}
|
||||
|
||||
fn appendField(
|
||||
node: Node,
|
||||
Table: type,
|
||||
relations: []const type,
|
||||
comptime len: usize,
|
||||
fields_array: *[len]Field,
|
||||
tuple_index: *usize,
|
||||
) void {
|
||||
switch (node) {
|
||||
.condition => {},
|
||||
.value => |value| {
|
||||
appendValueField(value.ColumnType(), value, len, fields_array, tuple_index);
|
||||
},
|
||||
.group => |group| {
|
||||
for (group.children) |child| {
|
||||
appendField(child, Table, relations, len, fields_array, tuple_index);
|
||||
}
|
||||
},
|
||||
.triplet => |triplet| {
|
||||
switch (triplet.lhs) {
|
||||
.value => |value| {
|
||||
appendValueField(value.type, value, len, fields_array, tuple_index);
|
||||
},
|
||||
.column => {},
|
||||
}
|
||||
switch (triplet.rhs) {
|
||||
.value => |value| {
|
||||
appendValueField(value.type, value, len, fields_array, tuple_index);
|
||||
},
|
||||
.column => {},
|
||||
}
|
||||
},
|
||||
.sql_string => |sql_string| {
|
||||
for (sql_string.values) |value_node| {
|
||||
const value = value_node.value;
|
||||
appendValueField(value.type, value, len, fields_array, tuple_index);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn countValues(comptime self: Node) usize {
|
||||
var count: usize = 0;
|
||||
countNodeValues(self, &count);
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
fn countNodeValues(comptime node: Node, count: *usize) void {
|
||||
switch (node) {
|
||||
.condition => {},
|
||||
.value => |value| {
|
||||
if (!value.isNull()) count.* += 1;
|
||||
},
|
||||
.group => |group| {
|
||||
for (group.children) |child| countNodeValues(child, count);
|
||||
},
|
||||
.triplet => |triplet| {
|
||||
switch (triplet.lhs) {
|
||||
.value => |value| {
|
||||
if (!value.isNull()) count.* += 1;
|
||||
},
|
||||
.column => {},
|
||||
}
|
||||
switch (triplet.rhs) {
|
||||
.value => |value| {
|
||||
if (!value.isNull()) count.* += 1;
|
||||
},
|
||||
.column => {},
|
||||
}
|
||||
},
|
||||
.sql_string => |sql_string| {
|
||||
for (sql_string.values) |value_node| {
|
||||
countNodeValues(value_node, count);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
fn nodeTree(
|
||||
Adapter: type,
|
||||
Table: type,
|
||||
relations: []const type,
|
||||
OG: type,
|
||||
T: type,
|
||||
comptime name: []const u8,
|
||||
field_info: std.builtin.Type.StructField,
|
||||
comptime path: [][]const u8,
|
||||
comptime field_context: fields.FieldContext,
|
||||
comptime value_index: *usize,
|
||||
) Node {
|
||||
comptime {
|
||||
if (coercion.canCoerceDelegate(T)) {
|
||||
const value = Node.Value{
|
||||
.field_context = field_context,
|
||||
.Table = findRelation(Table, relations, path),
|
||||
.name = name,
|
||||
.type = T,
|
||||
.field_info = field_info,
|
||||
.index = value_index.*,
|
||||
};
|
||||
value_index.* += 1;
|
||||
return .{ .value = value };
|
||||
}
|
||||
|
||||
return switch (@typeInfo(T)) {
|
||||
.@"struct" => |info| if (isTriplet(T)) blk: {
|
||||
break :blk .{ .triplet = makeTriplet(
|
||||
Adapter,
|
||||
Table,
|
||||
relations,
|
||||
T,
|
||||
field_context,
|
||||
field_info,
|
||||
name,
|
||||
path,
|
||||
value_index,
|
||||
) };
|
||||
} else if (isSqlString(T)) blk: {
|
||||
const t: T = undefined;
|
||||
const value_fields = std.meta.fields(@TypeOf(t[1]));
|
||||
var nodes: [value_fields.len]Node = undefined;
|
||||
for (value_fields, 0..) |value_field, index| {
|
||||
nodes[index] = .{
|
||||
.value = .{
|
||||
// Name is used for type coercion, for an SQL string arg we don't
|
||||
// have a target column so name is not useful.
|
||||
.name = "_",
|
||||
.type = fields.ComptimeErasedType(value_field.type),
|
||||
.Table = Table,
|
||||
.field_info = fields.ComptimeErasedStructField(value_field),
|
||||
.field_context = field_context,
|
||||
.index = value_index.*,
|
||||
},
|
||||
};
|
||||
value_index.* += 1;
|
||||
}
|
||||
const final = nodes;
|
||||
break :blk .{ .sql_string = .{ .sql = t[0], .values = &final } };
|
||||
} else blk: {
|
||||
const nodes = childNodes(
|
||||
Adapter,
|
||||
Table,
|
||||
relations,
|
||||
OG,
|
||||
field_info,
|
||||
info,
|
||||
path,
|
||||
field_context,
|
||||
value_index,
|
||||
);
|
||||
break :blk .{ .group = .{ .name = name, .children = &nodes } };
|
||||
},
|
||||
.enum_literal => blk: {
|
||||
if (path.len == 0) unreachable;
|
||||
|
||||
var t: type = OG;
|
||||
for (path[0 .. path.len - 1]) |c| {
|
||||
t = std.meta.FieldType(t, std.enums.nameCast(std.meta.FieldEnum(t), c));
|
||||
}
|
||||
const value: t = undefined;
|
||||
const condition = @field(value, path[path.len - 1]);
|
||||
break :blk .{ .condition = condition };
|
||||
},
|
||||
.null => .{
|
||||
.value = .{
|
||||
.field_context = field_context,
|
||||
.Table = findRelation(Table, relations, path),
|
||||
.name = name,
|
||||
.type = T,
|
||||
.field_info = field_info,
|
||||
// We write `IS NULL` directly into the SQL without a bind param
|
||||
.index = undefined,
|
||||
},
|
||||
},
|
||||
else => blk: {
|
||||
const value = Node.Value{
|
||||
.field_context = field_context,
|
||||
.Table = findRelation(Table, relations, path),
|
||||
.name = name,
|
||||
.type = T,
|
||||
.field_info = field_info,
|
||||
.index = value_index.*,
|
||||
};
|
||||
value_index.* += 1;
|
||||
break :blk .{ .value = value };
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn childNodes(
|
||||
Adapter: type,
|
||||
Table: type,
|
||||
relations: []const type,
|
||||
OG: type,
|
||||
comptime field_info: std.builtin.Type.StructField,
|
||||
comptime struct_info: std.builtin.Type.Struct,
|
||||
comptime path: [][]const u8,
|
||||
comptime field_context: fields.FieldContext,
|
||||
comptime value_index: *usize,
|
||||
) [struct_info.fields.len]Node {
|
||||
var nodes: [struct_info.fields.len]Node = undefined;
|
||||
|
||||
for (struct_info.fields, 0..) |field, index| {
|
||||
var appended_path: [path.len + 1][]const u8 = undefined;
|
||||
for (0..path.len) |idx| appended_path[idx] = path[idx];
|
||||
appended_path[appended_path.len - 1] = field.name;
|
||||
nodes[index] = nodeTree(
|
||||
Adapter,
|
||||
Table,
|
||||
relations,
|
||||
OG,
|
||||
field.type,
|
||||
field.name,
|
||||
field_info,
|
||||
&appended_path,
|
||||
field_context,
|
||||
value_index,
|
||||
);
|
||||
}
|
||||
|
||||
return nodes;
|
||||
}
|
||||
|
||||
fn findRelation(Table: type, relations: []const type, comptime path: [][]const u8) type {
|
||||
comptime {
|
||||
if (path.len <= 1) return Table;
|
||||
for (relations) |relation| {
|
||||
if (std.mem.eql(u8, relation.relation_name, path[path.len - 2])) return relation.Source;
|
||||
}
|
||||
return Table;
|
||||
}
|
||||
}
|
||||
|
||||
fn assignValues(
|
||||
arg: anytype,
|
||||
ValuesTuple: type,
|
||||
values_tuple: *ValuesTuple,
|
||||
ErrorsTuple: type,
|
||||
errors_tuple: *ErrorsTuple,
|
||||
values_fields: []const Field,
|
||||
comptime tuple_index: usize,
|
||||
comptime coerce: bool,
|
||||
) void {
|
||||
if (comptime coercion.canCoerceDelegate(@TypeOf(arg))) {
|
||||
assignValue(
|
||||
arg,
|
||||
ValuesTuple,
|
||||
values_tuple,
|
||||
ErrorsTuple,
|
||||
errors_tuple,
|
||||
values_fields,
|
||||
tuple_index,
|
||||
coerce,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
comptime var idx: usize = tuple_index;
|
||||
|
||||
switch (@typeInfo(@TypeOf(arg))) {
|
||||
.@"struct" => |info| {
|
||||
// XXX: Note that we will always land here first because the first arg to `where` is
|
||||
// is always a struct, so we can safely start here for incrementing our index
|
||||
// counter.
|
||||
if (comptime isSqlString(@TypeOf(arg))) {
|
||||
assignValues(
|
||||
arg[1],
|
||||
ValuesTuple,
|
||||
values_tuple,
|
||||
ErrorsTuple,
|
||||
errors_tuple,
|
||||
values_fields,
|
||||
idx,
|
||||
false,
|
||||
);
|
||||
} else {
|
||||
inline for (info.fields) |field| {
|
||||
assignValues(
|
||||
@field(arg, field.name),
|
||||
ValuesTuple,
|
||||
values_tuple,
|
||||
ErrorsTuple,
|
||||
errors_tuple,
|
||||
values_fields,
|
||||
idx,
|
||||
coerce,
|
||||
);
|
||||
comptime detectValues(field.type, &idx);
|
||||
}
|
||||
}
|
||||
},
|
||||
.type => {},
|
||||
.enum_literal => {},
|
||||
.null => {},
|
||||
else => {
|
||||
assignValue(
|
||||
arg,
|
||||
ValuesTuple,
|
||||
values_tuple,
|
||||
ErrorsTuple,
|
||||
errors_tuple,
|
||||
values_fields,
|
||||
tuple_index,
|
||||
coerce,
|
||||
);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn detectValues(T: type, index: *usize) void {
|
||||
comptime {
|
||||
if (coercion.canCoerceDelegate(T)) {
|
||||
index.* += 1;
|
||||
return;
|
||||
}
|
||||
switch (@typeInfo(T)) {
|
||||
.@"struct" => |info| {
|
||||
for (info.fields) |field| detectValues(field.type, index);
|
||||
},
|
||||
.type, .enum_literal, .null => {},
|
||||
else => index.* += 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn assignValue(
|
||||
arg: anytype,
|
||||
ValuesTuple: type,
|
||||
values_tuple: *ValuesTuple,
|
||||
ErrorsTuple: type,
|
||||
errors_tuple: *ErrorsTuple,
|
||||
values_fields: []const Field,
|
||||
comptime tuple_index: usize,
|
||||
comptime coerce: bool,
|
||||
) void {
|
||||
inline for (
|
||||
std.meta.fields(ValuesTuple),
|
||||
values_fields,
|
||||
0..,
|
||||
) |field, value_field, index| {
|
||||
const tuple_field_name = std.fmt.comptimePrint("{d}", .{tuple_index});
|
||||
|
||||
if (comptime tuple_index == index) {
|
||||
const field_info = comptime fields.fieldInfo(
|
||||
field,
|
||||
value_field.Table,
|
||||
value_field.name,
|
||||
value_field.context,
|
||||
);
|
||||
if (comptime coerce) {
|
||||
const coerced: coercion.CoercedValue(
|
||||
value_field.column_type,
|
||||
@TypeOf(arg),
|
||||
) = coercion.coerce(
|
||||
value_field.Table,
|
||||
field_info,
|
||||
arg,
|
||||
);
|
||||
@field(values_tuple, tuple_field_name) = coerced.value;
|
||||
@field(errors_tuple, tuple_field_name) = coerced.err;
|
||||
} else {
|
||||
// When an SQL string is used we don't have a target type to coerce to, so assign
|
||||
// the value directly - user is responsible for coercing to appropriate types.
|
||||
@field(values_tuple, tuple_field_name) = arg;
|
||||
@field(errors_tuple, tuple_field_name) = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A triplet with an operator, e.g.:
|
||||
// ```zig
|
||||
// .{ .foo, .eql, .bar }
|
||||
// .{ sql.max(.foo), .lt, .bar }
|
||||
fn isTriplet(T: type) bool {
|
||||
const struct_fields = std.meta.fields(T);
|
||||
|
||||
if (struct_fields.len != 3) return false;
|
||||
if (@typeInfo(struct_fields[1].type) != .enum_literal) return false;
|
||||
|
||||
const t: T = undefined;
|
||||
return @hasField(Node.Triplet.Operator, @tagName(t[1]));
|
||||
}
|
||||
|
||||
// A pair of comptime SQL string and a tuple of values, e.g.:
|
||||
// ```zig
|
||||
// .{ "foo = ?", .{1} }
|
||||
// ```
|
||||
fn isSqlString(T: type) bool {
|
||||
const struct_fields = std.meta.fields(T);
|
||||
|
||||
if (struct_fields.len != 2) return false;
|
||||
if (@typeInfo(struct_fields[1].type) != .@"struct") return false;
|
||||
|
||||
const is_string = switch (@typeInfo(struct_fields[0].type)) {
|
||||
.pointer => |info| switch (@typeInfo(info.child)) {
|
||||
.array => |array_info| info.is_volatile == false and
|
||||
array_info.child == u8 and
|
||||
(info.size == .Slice or info.size == .One),
|
||||
.int => |int_info| info.is_volatile == false and
|
||||
int_info.signedness == .unsigned and
|
||||
int_info.bits == 8,
|
||||
else => false,
|
||||
},
|
||||
else => false,
|
||||
};
|
||||
|
||||
if (!is_string) return false;
|
||||
|
||||
return if (!struct_fields[0].is_comptime) @compileError(
|
||||
"Custom string clauses must be comptime-known.",
|
||||
) else true;
|
||||
}
|
||||
|
||||
fn makeTriplet(
|
||||
Adapter: type,
|
||||
Table: type,
|
||||
relations: []const type,
|
||||
T: type,
|
||||
comptime field_context: fields.FieldContext,
|
||||
comptime field_info: std.builtin.Type.StructField,
|
||||
comptime name: []const u8,
|
||||
comptime path: [][]const u8,
|
||||
comptime value_index: *usize,
|
||||
) Node.Triplet {
|
||||
const arg: T = undefined;
|
||||
|
||||
return .{
|
||||
.lhs = makeOperand(
|
||||
Adapter,
|
||||
Table,
|
||||
T,
|
||||
if (@TypeOf(arg[2]) == type) arg[2] else @TypeOf(arg[2]),
|
||||
0,
|
||||
2,
|
||||
relations,
|
||||
field_context,
|
||||
name,
|
||||
path,
|
||||
field_info,
|
||||
value_index,
|
||||
),
|
||||
.operator = std.enums.nameCast(Node.Triplet.Operator, arg[1]),
|
||||
.rhs = makeOperand(
|
||||
Adapter,
|
||||
Table,
|
||||
T,
|
||||
if (@TypeOf(arg[0]) == type) arg[0] else @TypeOf(arg[0]),
|
||||
2,
|
||||
0,
|
||||
relations,
|
||||
field_context,
|
||||
name,
|
||||
path,
|
||||
field_info,
|
||||
value_index,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
fn makeOperand(
|
||||
Adapter: type,
|
||||
Table: type,
|
||||
T: type,
|
||||
Other: type,
|
||||
comptime arg_index: usize,
|
||||
comptime other_arg_index: usize,
|
||||
relations: []const type,
|
||||
comptime field_context: fields.FieldContext,
|
||||
comptime name: []const u8,
|
||||
comptime path: [][]const u8,
|
||||
comptime field_info: std.builtin.Type.StructField,
|
||||
comptime value_index: *usize,
|
||||
) Node.Triplet.Operand {
|
||||
const arg: T = undefined;
|
||||
|
||||
return switch (@typeInfo(@TypeOf(arg[arg_index]))) {
|
||||
.enum_literal => .{
|
||||
.column = columns.translate(Table, relations, .{arg[arg_index]})[0],
|
||||
},
|
||||
.type => functionColumn(arg[arg_index], Table, relations),
|
||||
else => blk: {
|
||||
const A = switch (@typeInfo(Other)) {
|
||||
.type => Adapter.Aggregate(functionColumn(Other).function.?),
|
||||
.enum_literal => enum_blk: {
|
||||
const column = columns.translate(Table, relations, .{arg[other_arg_index]})[0];
|
||||
break :enum_blk column.type;
|
||||
},
|
||||
// We're comparing two values (not a column and a value),
|
||||
// let Zig reconsile the types:
|
||||
else => fields.ComptimeErasedType(@TypeOf(arg[arg_index])),
|
||||
};
|
||||
const value: Node.Value = .{
|
||||
.field_context = field_context,
|
||||
.Table = findRelation(Table, relations, path),
|
||||
.name = name,
|
||||
.type = A,
|
||||
.field_info = field_info,
|
||||
.index = value_index.*,
|
||||
};
|
||||
value_index.* += 1;
|
||||
break :blk .{ .value = value };
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
fn functionColumn(T: type, Table: type, relations: []const type) Node.Triplet.Operand {
|
||||
return if (@hasField(T, "__jetquery_function"))
|
||||
.{ .column = columns.translate(
|
||||
Table,
|
||||
relations,
|
||||
.{T},
|
||||
)[0] }
|
||||
else
|
||||
@compileError("Unexpected type in clause: `" ++ @typeName(T) ++ "`");
|
||||
}
|
||||
|
||||
fn debugNode(comptime node: Node, comptime depth: usize) void {
|
||||
const indent = " " ** depth;
|
||||
switch (node) {
|
||||
.condition => |value| {
|
||||
@compileLog(std.fmt.comptimePrint("{s}{s}", .{ indent, @tagName(value) }));
|
||||
},
|
||||
.value => |value| {
|
||||
@compileLog(std.fmt.comptimePrint("{s}{s}", .{ indent, value.name }));
|
||||
},
|
||||
.group => |group| {
|
||||
for (group.children) |child| {
|
||||
debugNode(child, depth + 1);
|
||||
}
|
||||
},
|
||||
.triplet => |triplet| {
|
||||
@compileLog(std.fmt.comptimePrint("{s}{s}{s}{s}", .{
|
||||
indent,
|
||||
@tagName(triplet.lhs),
|
||||
@tagName(triplet.operator),
|
||||
@tagName(triplet.rhs),
|
||||
}));
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn debug(value: anytype) void {
|
||||
@compileLog(std.fmt.comptimePrint("{any}", .{value}));
|
||||
}
|
||||
660
src/jetquery/sql/render.zig
Normal file
660
src/jetquery/sql/render.zig
Normal file
@ -0,0 +1,660 @@
|
||||
const std = @import("std");
|
||||
|
||||
const jetquery = @import("../../jetquery.zig");
|
||||
|
||||
pub fn render(
|
||||
Adapter: type,
|
||||
query_context: jetquery.sql.QueryContext,
|
||||
Table: type,
|
||||
relations: []const type,
|
||||
comptime field_infos: []const jetquery.fields.FieldInfo,
|
||||
comptime columns: []const jetquery.columns.Column,
|
||||
comptime order_clauses: []const jetquery.sql.OrderClause,
|
||||
comptime distinct: ?[]const jetquery.columns.Column,
|
||||
comptime where_clauses: []const jetquery.sql.Where.Tree,
|
||||
comptime group_by: ?[]const jetquery.columns.Column,
|
||||
comptime having_clauses: []const jetquery.sql.Where.Tree,
|
||||
) []const u8 {
|
||||
return switch (query_context) {
|
||||
.select => renderSelect(
|
||||
Adapter,
|
||||
Table,
|
||||
relations,
|
||||
columns,
|
||||
field_infos,
|
||||
order_clauses,
|
||||
where_clauses,
|
||||
group_by,
|
||||
having_clauses,
|
||||
),
|
||||
.update => renderUpdate(
|
||||
Adapter,
|
||||
Table,
|
||||
where_clauses,
|
||||
field_infos,
|
||||
),
|
||||
.insert => renderInsert(
|
||||
Adapter,
|
||||
Table,
|
||||
field_infos,
|
||||
),
|
||||
.delete, .delete_all => renderDelete(
|
||||
Adapter,
|
||||
Table,
|
||||
field_infos,
|
||||
where_clauses,
|
||||
query_context,
|
||||
),
|
||||
.count => renderCount(
|
||||
Adapter,
|
||||
Table,
|
||||
relations,
|
||||
field_infos,
|
||||
where_clauses,
|
||||
distinct,
|
||||
),
|
||||
.none => "",
|
||||
};
|
||||
}
|
||||
|
||||
fn renderSelect(
|
||||
Adapter: type,
|
||||
Table: type,
|
||||
relations: []const type,
|
||||
comptime columns: []const jetquery.columns.Column,
|
||||
comptime field_infos: []const jetquery.fields.FieldInfo,
|
||||
comptime order_clauses: []const jetquery.sql.OrderClause,
|
||||
comptime where_clauses: []const jetquery.sql.Where.Tree,
|
||||
comptime group_by: ?[]const jetquery.columns.Column,
|
||||
comptime having_clauses: []const jetquery.sql.Where.Tree,
|
||||
) []const u8 {
|
||||
comptime {
|
||||
const select_columns = renderSelectColumns(Adapter, relations, columns);
|
||||
const from = std.fmt.comptimePrint(" FROM {s}", .{Adapter.identifier(Table.name)});
|
||||
const joins = renderJoins(Adapter, Table, relations);
|
||||
|
||||
return std.fmt.comptimePrint(
|
||||
"SELECT{s}{s}{s}{s}{s}{s}{s}",
|
||||
.{
|
||||
select_columns,
|
||||
from,
|
||||
joins,
|
||||
renderWhere(Adapter, where_clauses),
|
||||
renderGroupBy(Adapter, group_by, having_clauses),
|
||||
renderOrder(Adapter, order_clauses),
|
||||
renderLimit(Adapter, field_infos),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn renderUpdate(
|
||||
Adapter: type,
|
||||
Table: type,
|
||||
where_clauses: []const jetquery.sql.Where.Tree,
|
||||
comptime field_infos: []const jetquery.fields.FieldInfo,
|
||||
) []const u8 {
|
||||
var buf: [paramsBufSize(Adapter, field_infos, .update, .assign)]u8 = undefined;
|
||||
return std.fmt.comptimePrint(
|
||||
"UPDATE {s} SET {s}{s}",
|
||||
.{
|
||||
Adapter.identifier(Table.name),
|
||||
renderParams(&buf, Adapter, field_infos, .update, .assign),
|
||||
renderWhere(Adapter, where_clauses),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
pub fn renderUpdateRuntime(
|
||||
allocator: std.mem.Allocator,
|
||||
comptime Adapter: type,
|
||||
comptime Table: type,
|
||||
comptime where_clauses: []const jetquery.sql.Where.Tree,
|
||||
comptime Args: type,
|
||||
field_states: []const jetquery.sql.FieldState,
|
||||
first_value_index: usize,
|
||||
) ![]const u8 {
|
||||
var value_index = first_value_index;
|
||||
|
||||
var params_buf = std.ArrayList(u8).init(allocator);
|
||||
defer params_buf.deinit();
|
||||
const params_writer = params_buf.writer();
|
||||
|
||||
inline for (std.meta.fields(Args), 0..) |field, index| {
|
||||
if (field_states[index].modified) {
|
||||
var param_buf: [8]u8 = undefined;
|
||||
try params_writer.print(
|
||||
"{s}{s} = {s}",
|
||||
.{
|
||||
if (value_index > first_value_index) ", " else "",
|
||||
Adapter.identifier(field.name),
|
||||
try Adapter.paramSqlBuf(¶m_buf, value_index),
|
||||
},
|
||||
);
|
||||
value_index += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return try std.fmt.allocPrint(
|
||||
allocator,
|
||||
"UPDATE {s} SET {s}{s}",
|
||||
.{
|
||||
Adapter.identifier(Table.name),
|
||||
params_buf.items,
|
||||
comptime renderWhere(Adapter, where_clauses),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn renderInsert(
|
||||
Adapter: type,
|
||||
Table: type,
|
||||
comptime field_infos: []const jetquery.fields.FieldInfo,
|
||||
) []const u8 {
|
||||
var params_buf: [paramsBufSize(Adapter, field_infos, .insert, .column)]u8 = undefined;
|
||||
var values_buf: [paramsBufSize(Adapter, field_infos, .insert, .value)]u8 = undefined;
|
||||
return std.fmt.comptimePrint(
|
||||
"INSERT INTO {s} ({s}) VALUES ({s})",
|
||||
.{
|
||||
Adapter.identifier(Table.name),
|
||||
renderParams(¶ms_buf, Adapter, field_infos, .insert, .column),
|
||||
renderParams(&values_buf, Adapter, field_infos, .insert, .value),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn renderDelete(
|
||||
Adapter: type,
|
||||
Table: type,
|
||||
comptime field_infos: []const jetquery.fields.FieldInfo,
|
||||
comptime where_clauses: []const jetquery.sql.Where.Tree,
|
||||
comptime query_context: jetquery.sql.QueryContext,
|
||||
) []const u8 {
|
||||
const statement = std.fmt.comptimePrint("DELETE FROM {s}", .{Adapter.identifier(Table.name)});
|
||||
return switch (query_context) {
|
||||
.delete, .delete_all => std.fmt.comptimePrint("{s}{s}{s}", .{
|
||||
statement,
|
||||
renderWhere(Adapter, where_clauses),
|
||||
renderLimit(Adapter, field_infos),
|
||||
}),
|
||||
else => |tag| @compileError(
|
||||
"Inconsistent query for DELETE: `" ++ @tagName(tag) ++ "` (this is a bug)",
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
fn renderCount(
|
||||
Adapter: type,
|
||||
Table: type,
|
||||
comptime relations: []const type,
|
||||
comptime field_infos: []const jetquery.fields.FieldInfo,
|
||||
comptime where_clauses: []const jetquery.sql.Where.Tree,
|
||||
comptime distinct: ?[]const jetquery.columns.Column,
|
||||
) []const u8 {
|
||||
comptime {
|
||||
const count_column = " " ++ Adapter.countSql(distinct);
|
||||
const from = std.fmt.comptimePrint(" FROM {s}", .{Adapter.identifier(Table.name)});
|
||||
const joins = renderJoins(Adapter, Table, relations);
|
||||
|
||||
return std.fmt.comptimePrint(
|
||||
"SELECT{s}{s}{s}{s}{s}",
|
||||
.{
|
||||
count_column,
|
||||
from,
|
||||
joins,
|
||||
renderWhere(Adapter, where_clauses),
|
||||
renderLimit(Adapter, field_infos),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn renderLimit(
|
||||
Adapter: type,
|
||||
comptime field_infos: []const jetquery.fields.FieldInfo,
|
||||
) []const u8 {
|
||||
if (!hasParam(field_infos, .limit)) return "";
|
||||
|
||||
const offset = renderOffset(Adapter, field_infos);
|
||||
|
||||
return std.fmt.comptimePrint(
|
||||
" LIMIT {s}{s}",
|
||||
.{ Adapter.paramSql(lastParamIndex(field_infos, .limit)), offset },
|
||||
);
|
||||
}
|
||||
|
||||
fn renderOffset(
|
||||
Adapter: type,
|
||||
comptime field_infos: []const jetquery.fields.FieldInfo,
|
||||
) []const u8 {
|
||||
if (!hasParam(field_infos, .offset)) return "";
|
||||
|
||||
return std.fmt.comptimePrint(
|
||||
" OFFSET {s}",
|
||||
.{Adapter.paramSql(lastParamIndex(field_infos, .offset))},
|
||||
);
|
||||
}
|
||||
fn renderOrder(
|
||||
Adapter: type,
|
||||
comptime order_clauses: []const jetquery.sql.OrderClause,
|
||||
) []const u8 {
|
||||
if (order_clauses.len == 0) return "";
|
||||
|
||||
var size: usize = 0;
|
||||
for (order_clauses, 0..) |order_clause, index| {
|
||||
const separator = if (index + 1 < order_clauses.len) ", " else "";
|
||||
size += (Adapter.orderSql(order_clause) ++ separator).len;
|
||||
}
|
||||
var order_buf: [size]u8 = undefined;
|
||||
var cursor: usize = 0;
|
||||
for (order_clauses, 0..) |order_clause, index| {
|
||||
const separator = if (index + 1 < order_clauses.len) ", " else "";
|
||||
const order_sql = Adapter.orderSql(order_clause) ++ separator;
|
||||
@memcpy(order_buf[cursor .. cursor + order_sql.len], order_sql);
|
||||
cursor += order_sql.len;
|
||||
}
|
||||
return std.fmt.comptimePrint(
|
||||
" ORDER BY {s}",
|
||||
.{order_buf},
|
||||
);
|
||||
}
|
||||
|
||||
fn renderGroupBy(
|
||||
Adapter: type,
|
||||
comptime maybe_group_by: ?[]const jetquery.columns.Column,
|
||||
comptime having_clauses: []const jetquery.sql.Where.Tree,
|
||||
) []const u8 {
|
||||
const group_by = maybe_group_by orelse return "";
|
||||
|
||||
var size: usize = 0;
|
||||
|
||||
for (group_by, 0..) |column, index| {
|
||||
const separator = if (index + 1 < group_by.len) ", " else "";
|
||||
const column_sql = Adapter.columnSql(column) ++ separator;
|
||||
size += column_sql.len;
|
||||
}
|
||||
|
||||
const and_operator = " AND ";
|
||||
const having = " HAVING ";
|
||||
if (having_clauses.len > 0) size += having.len;
|
||||
for (having_clauses, 0..) |clause, index| {
|
||||
if (index > 0) size += and_operator.len;
|
||||
size += clause.render(Adapter).len;
|
||||
}
|
||||
|
||||
var buf: [size]u8 = undefined;
|
||||
var cursor: usize = 0;
|
||||
|
||||
for (group_by, 0..) |column, index| {
|
||||
const separator = if (index + 1 < group_by.len) ", " else "";
|
||||
const column_sql = Adapter.columnSql(column) ++ separator;
|
||||
@memcpy(buf[cursor .. cursor + column_sql.len], column_sql);
|
||||
cursor += column_sql.len;
|
||||
}
|
||||
|
||||
if (having_clauses.len > 0) {
|
||||
@memcpy(buf[cursor .. cursor + having.len], having);
|
||||
cursor += having.len;
|
||||
}
|
||||
|
||||
for (having_clauses, 0..) |clause, index| {
|
||||
if (index > 0) size += and_operator.len;
|
||||
const operator = if (index > 0) and_operator else "";
|
||||
const sql = operator ++ clause.render(Adapter);
|
||||
@memcpy(buf[cursor .. cursor + sql.len], sql);
|
||||
cursor += sql.len;
|
||||
}
|
||||
|
||||
return std.fmt.comptimePrint(" GROUP BY {s}", .{buf});
|
||||
}
|
||||
|
||||
fn renderWhere(
|
||||
Adapter: type,
|
||||
comptime where_clauses: []const jetquery.sql.Where.Tree,
|
||||
) []const u8 {
|
||||
if (where_clauses.len == 0) return " WHERE " ++ Adapter.emptyWhereSql();
|
||||
|
||||
const and_operator = " AND ";
|
||||
var size: usize = 0;
|
||||
for (where_clauses, 0..) |clause, index| {
|
||||
if (index > 0) size += and_operator.len;
|
||||
size += clause.render(Adapter).len;
|
||||
}
|
||||
var buf: [size]u8 = undefined;
|
||||
var cursor: usize = 0;
|
||||
for (where_clauses, 0..) |clause, index| {
|
||||
const operator = if (index > 0) and_operator else "";
|
||||
const sql = operator ++ clause.render(Adapter);
|
||||
@memcpy(buf[cursor .. cursor + sql.len], sql);
|
||||
cursor += sql.len;
|
||||
}
|
||||
|
||||
return std.fmt.comptimePrint(" WHERE {s}", .{&buf});
|
||||
}
|
||||
|
||||
fn renderJoins(Adapter: type, Table: type, relations: []const type) []const u8 {
|
||||
comptime {
|
||||
if (relations.len == 0) return "";
|
||||
|
||||
var buf_len: usize = 0;
|
||||
for (relations) |Relation| {
|
||||
buf_len += switch (Relation.context) {
|
||||
.inner => renderInnerJoin(Adapter, Table, Relation).len,
|
||||
.outer => renderOuterJoin(Adapter, Table, Relation).len,
|
||||
.include => switch (Relation.relation_type) {
|
||||
.belongs_to => renderInnerJoin(Adapter, Table, Relation).len,
|
||||
.has_many => "".len,
|
||||
},
|
||||
};
|
||||
}
|
||||
var buf: [buf_len]u8 = undefined;
|
||||
var cursor: usize = 0;
|
||||
for (relations) |Relation| {
|
||||
const sql = switch (Relation.context) {
|
||||
.inner => renderInnerJoin(Adapter, Table, Relation),
|
||||
.outer => renderOuterJoin(Adapter, Table, Relation),
|
||||
.include => switch (Relation.relation_type) {
|
||||
.belongs_to => renderInnerJoin(Adapter, Table, Relation),
|
||||
.has_many => "", // We issue separate queries for has_many
|
||||
},
|
||||
};
|
||||
@memcpy(buf[cursor .. cursor + sql.len], sql);
|
||||
cursor += sql.len;
|
||||
}
|
||||
|
||||
return &buf;
|
||||
}
|
||||
}
|
||||
|
||||
fn renderInnerJoin(Adapter: type, Table: type, Relation: type) []const u8 {
|
||||
const PrimaryKey = std.meta.FieldEnum(Relation.Source.Definition);
|
||||
const ForeignKey = std.meta.FieldEnum(Table.Definition);
|
||||
const primary_key: PrimaryKey = std.enums.nameCast(
|
||||
PrimaryKey,
|
||||
switch (Relation.relation_type) {
|
||||
.belongs_to => Relation.primary_key,
|
||||
.has_many => Table.defaultForeignKey(),
|
||||
},
|
||||
);
|
||||
const foreign_key: ForeignKey = std.enums.nameCast(
|
||||
ForeignKey,
|
||||
Relation.foreign_key orelse switch (Relation.relation_type) {
|
||||
.belongs_to => Table.primary_key,
|
||||
.has_many => Relation.Source.primary_key,
|
||||
},
|
||||
);
|
||||
|
||||
return Adapter.innerJoinSql(
|
||||
Table,
|
||||
Relation.Source,
|
||||
Relation.relation_name,
|
||||
.{ .primary_key = @tagName(primary_key), .foreign_key = @tagName(foreign_key) },
|
||||
);
|
||||
}
|
||||
|
||||
fn renderOuterJoin(Adapter: type, Table: type, Relation: type) []const u8 {
|
||||
const PrimaryKey = std.meta.FieldEnum(Relation.Source.Definition);
|
||||
const ForeignKey = std.meta.FieldEnum(Table.Definition);
|
||||
const primary_key: PrimaryKey = std.enums.nameCast(
|
||||
PrimaryKey,
|
||||
switch (Relation.relation_type) {
|
||||
.belongs_to => Relation.primary_key,
|
||||
.has_many => Table.defaultForeignKey(),
|
||||
},
|
||||
);
|
||||
const foreign_key: ForeignKey = std.enums.nameCast(
|
||||
ForeignKey,
|
||||
Relation.foreign_key orelse switch (Relation.relation_type) {
|
||||
.belongs_to => Table.primary_key,
|
||||
.has_many => Relation.Source.primary_key,
|
||||
},
|
||||
);
|
||||
|
||||
return Adapter.outerJoinSql(
|
||||
Table,
|
||||
Relation.Source,
|
||||
Relation.relation_name,
|
||||
.{ .primary_key = @tagName(primary_key), .foreign_key = @tagName(foreign_key) },
|
||||
);
|
||||
}
|
||||
|
||||
fn renderSelectColumns(
|
||||
Adapter: type,
|
||||
relations: []const type,
|
||||
comptime columns: []const jetquery.columns.Column,
|
||||
) []const u8 {
|
||||
comptime {
|
||||
var total_columns: usize = columns.len;
|
||||
for (relations) |Relation| {
|
||||
// has_many relations issue a separate query so we don't select columns here.
|
||||
// Only belongs_to uses an inner join.
|
||||
if (Relation.relation_type != .belongs_to) continue;
|
||||
total_columns += Relation.select_columns.len;
|
||||
}
|
||||
|
||||
var columns_buf_len: usize = 0;
|
||||
for (columns, 0..) |column, index| {
|
||||
columns_buf_len += renderSelectColumn(
|
||||
Adapter,
|
||||
column,
|
||||
index,
|
||||
total_columns,
|
||||
).len;
|
||||
}
|
||||
|
||||
var start = columns.len;
|
||||
for (relations) |Relation| {
|
||||
// has_many relations issue a separate query so we don't select columns here.
|
||||
// Only belongs_to uses an inner join.
|
||||
if (Relation.relation_type != .belongs_to) continue;
|
||||
|
||||
for (Relation.select_columns, start..) |column, index| {
|
||||
columns_buf_len += renderSelectColumn(
|
||||
Adapter,
|
||||
column,
|
||||
index,
|
||||
total_columns,
|
||||
).len;
|
||||
}
|
||||
start += Relation.select_columns.len;
|
||||
}
|
||||
|
||||
var columns_buf: [columns_buf_len]u8 = undefined;
|
||||
var cursor: usize = 0;
|
||||
for (columns, 0..) |column, index| {
|
||||
const column_tag = renderSelectColumn(
|
||||
Adapter,
|
||||
column,
|
||||
index,
|
||||
total_columns,
|
||||
);
|
||||
@memcpy(columns_buf[cursor .. cursor + column_tag.len], column_tag);
|
||||
cursor += column_tag.len;
|
||||
}
|
||||
|
||||
start = columns.len;
|
||||
for (relations) |Relation| {
|
||||
// has_many relations issue a separate query so we don't select columns here.
|
||||
// Only belongs_to uses an inner join.
|
||||
if (Relation.relation_type != .belongs_to) continue;
|
||||
|
||||
for (Relation.select_columns, start..) |column, index| {
|
||||
const column_tag = renderSelectColumn(
|
||||
Adapter,
|
||||
column,
|
||||
index,
|
||||
total_columns,
|
||||
);
|
||||
@memcpy(columns_buf[cursor .. cursor + column_tag.len], column_tag);
|
||||
cursor += column_tag.len;
|
||||
}
|
||||
start += Relation.select_columns.len;
|
||||
}
|
||||
return &columns_buf;
|
||||
}
|
||||
}
|
||||
|
||||
fn renderSelectColumn(
|
||||
Adapter: type,
|
||||
comptime column: jetquery.columns.Column,
|
||||
comptime index: usize,
|
||||
comptime total: usize,
|
||||
) []const u8 {
|
||||
comptime {
|
||||
return std.fmt.comptimePrint(
|
||||
" {s}{s}",
|
||||
.{ Adapter.columnSql(column), if (index + 1 < total) "," else "" },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Find a nicer way of counting so we don't have to keep this sync'ed with `renderParams`
|
||||
fn paramsBufSize(
|
||||
Adapter: type,
|
||||
comptime field_infos: []const jetquery.fields.FieldInfo,
|
||||
comptime context: jetquery.fields.FieldContext,
|
||||
comptime format: enum { column, value, assign },
|
||||
) usize {
|
||||
var buf_len: usize = 0;
|
||||
|
||||
const separator = switch (context) {
|
||||
.where => " AND ",
|
||||
.update, .insert => ", ",
|
||||
else => @compileError("Unsupported param type: `" ++ @tagName(context) ++ "`"),
|
||||
};
|
||||
|
||||
const last_param_index = lastParamIndex(field_infos, context);
|
||||
|
||||
const template = switch (format) {
|
||||
.column, .value => "{s}{s}",
|
||||
.assign => "{s} = {s}{s}",
|
||||
};
|
||||
|
||||
for (field_infos, 0..) |field, index| {
|
||||
if (!fieldContext(field_infos, index, context)) continue;
|
||||
|
||||
const args = switch (format) {
|
||||
.column => .{
|
||||
switch (context) {
|
||||
.insert => Adapter.identifier(field.name),
|
||||
else => Adapter.columnSql(field),
|
||||
},
|
||||
if (index < last_param_index) separator else "",
|
||||
},
|
||||
.value => .{
|
||||
Adapter.paramSql(index),
|
||||
if (index < last_param_index) separator else "",
|
||||
},
|
||||
.assign => switch (context) {
|
||||
.update => .{
|
||||
Adapter.identifier(field.name),
|
||||
Adapter.paramSql(index),
|
||||
if (index < last_param_index) separator else "",
|
||||
},
|
||||
else => .{
|
||||
Adapter.columnSql(field),
|
||||
Adapter.paramSql(index),
|
||||
if (index < last_param_index) separator else "",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
buf_len += std.fmt.count(template, args);
|
||||
}
|
||||
return buf_len;
|
||||
}
|
||||
|
||||
fn renderParams(
|
||||
buf: []u8,
|
||||
Adapter: type,
|
||||
comptime field_infos: []const jetquery.fields.FieldInfo,
|
||||
comptime context: jetquery.fields.FieldContext,
|
||||
comptime format: enum { column, value, assign },
|
||||
) []const u8 {
|
||||
if (!hasParam(field_infos, context)) @compileError("Failed compiling UPDATE query with empty params.");
|
||||
|
||||
const separator = switch (context) {
|
||||
.where => " AND ",
|
||||
.update, .insert => ", ",
|
||||
else => @compileError("Unsupported param type: `" ++ @tagName(context) ++ "`"),
|
||||
};
|
||||
|
||||
const last_param_index = lastParamIndex(field_infos, context);
|
||||
var cursor: usize = 0;
|
||||
|
||||
const template = switch (format) {
|
||||
.column, .value => "{s}{s}",
|
||||
.assign => "{s} = {s}{s}",
|
||||
};
|
||||
|
||||
for (field_infos, 0..) |field, index| {
|
||||
if (!fieldContext(field_infos, index, context)) continue;
|
||||
|
||||
const args = switch (format) {
|
||||
.column => .{
|
||||
switch (context) {
|
||||
.insert => Adapter.identifier(field.name),
|
||||
else => Adapter.columnSql(field),
|
||||
},
|
||||
if (index < last_param_index) separator else "",
|
||||
},
|
||||
.value => .{
|
||||
Adapter.paramSql(index),
|
||||
if (index < last_param_index) separator else "",
|
||||
},
|
||||
.assign => switch (context) {
|
||||
.update => .{
|
||||
Adapter.identifier(field.name),
|
||||
Adapter.paramSql(index),
|
||||
if (index < last_param_index) separator else "",
|
||||
},
|
||||
else => .{
|
||||
Adapter.columnSql(field),
|
||||
Adapter.paramSql(index),
|
||||
if (index < last_param_index) separator else "",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const param = std.fmt.comptimePrint(template, args);
|
||||
@memcpy(buf[cursor .. cursor + param.len], param);
|
||||
cursor += param.len;
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
|
||||
fn fieldContext(
|
||||
comptime field_infos: []const jetquery.fields.FieldInfo,
|
||||
comptime index: usize,
|
||||
comptime context: jetquery.fields.FieldContext,
|
||||
) bool {
|
||||
return if (field_infos.len > index)
|
||||
field_infos[index].context == context
|
||||
else
|
||||
false;
|
||||
}
|
||||
|
||||
fn lastParamIndex(
|
||||
comptime field_infos: []const jetquery.fields.FieldInfo,
|
||||
comptime context: jetquery.fields.FieldContext,
|
||||
) usize {
|
||||
var maybe_index: ?usize = null;
|
||||
|
||||
for (field_infos, 0..) |field, index| {
|
||||
if (field.context == context) maybe_index = index;
|
||||
}
|
||||
if (maybe_index) |index|
|
||||
return index
|
||||
else
|
||||
@compileError("No param matched for `" ++ @tagName(context) ++ "` query.");
|
||||
}
|
||||
|
||||
fn hasParam(
|
||||
comptime field_infos: []const jetquery.fields.FieldInfo,
|
||||
comptime context: jetquery.fields.FieldContext,
|
||||
) bool {
|
||||
for (field_infos) |field| {
|
||||
if (field.context == context) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
6
src/jetquery/util.zig
Normal file
6
src/jetquery/util.zig
Normal file
@ -0,0 +1,6 @@
|
||||
pub inline fn stringMaybeEnum(arg: anytype) []const u8 {
|
||||
return switch (@typeInfo(@TypeOf(arg))) {
|
||||
.enum_literal => @tagName(arg),
|
||||
else => arg,
|
||||
};
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user