Sql refactor

This commit is contained in:
jaina heartles 2022-10-01 02:05:33 -07:00
parent c42039c559
commit c6d80fd4d3
8 changed files with 967 additions and 327 deletions

View File

@ -5,6 +5,56 @@ const Uuid = util.Uuid;
const DateTime = util.DateTime;
const Allocator = std.mem.Allocator;
const UnexpectedError = error{Unexpected};
const ConstraintError = error{
NotNullViolation,
ForeignKeyViolation,
UniqueViolation,
CheckViolation,
/// Catchall for miscellaneous types of constraints
ConstraintViolation,
};
pub const OpenError = error{BadConnection} || UnexpectedError;
pub const ExecError = error{
Cancelled,
ConnectionLost,
InternalException,
DatabaseBusy,
PermissionDenied,
SqlException,
/// Argument could not be marshalled for query
InvalidArgument,
/// An argument was not used by the query (not checked in all DB engines)
UndefinedParameter,
/// Memory error when marshalling argument for query
OutOfMemory,
AllocatorRequired,
} || ConstraintError || UnexpectedError;
pub const RowError = error{
Cancelled,
ConnectionLost,
InternalException,
DatabaseBusy,
PermissionDenied,
SqlException,
} || ConstraintError || UnexpectedError;
pub const GetError = error{
OutOfMemory,
AllocatorRequired,
TypeMismatch,
} || UnexpectedError;
pub const ColumnCountError = error{OutOfRange};
pub const ColumnIndexError = error{ NotFound, OutOfRange };
pub const QueryOptions = struct {
// If true, then it will not return an error on the SQLite backend
// if an argument passed does not map to a parameter in the query.
@ -32,7 +82,7 @@ pub fn prepareParamText(arena: *std.heap.ArenaAllocator, val: anytype) !?[:0]con
return switch (@TypeOf(val)) {
[:0]u8, [:0]const u8 => val,
[]const u8, []u8 => try std.cstr.addNullByte(val),
[]const u8, []u8 => try std.cstr.addNullByte(arena.allocator(), val),
DateTime, Uuid => try std.fmt.allocPrintZ(arena.allocator(), "{}", .{val}),
else => |T| switch (@typeInfo(T)) {
@ -44,10 +94,6 @@ pub fn prepareParamText(arena: *std.heap.ArenaAllocator, val: anytype) !?[:0]con
};
}
fn parseEnum(comptime T: type, _: []const u8) !T {
@panic("not implemented");
}
// Parse a (not-null) value from a string
pub fn parseValueNotNull(alloc: ?Allocator, comptime T: type, str: []const u8) !T {
return switch (T) {

View File

@ -0,0 +1,220 @@
const std = @import("std");
const util = @import("util");
const common = @import("./common.zig");
const c = @import("./postgres/c.zig");
const errors = @import("./postgres/errors.zig");
const Allocator = std.mem.Allocator;
pub const Results = struct {
result: *c.PGresult,
next_row_index: c_int = 0,
pub fn row(self: *Results) common.RowError!?Row {
if (self.next_row_index >= self.rowCount()) return null;
const idx = self.next_row_index;
self.next_row_index += 1;
return Row{
.result = self.result,
.row_index = idx,
};
}
pub fn columnCount(self: Results) common.ColumnCountError!u15 {
return std.math.cast(u15, c.PQnfields(self.result)) orelse error.OutOfRange;
}
pub fn columnIndex(self: Results, name: []const u8) common.ColumnIndexError!u15 {
const idx = c.PQfnumber(self.result, name.ptr);
if (idx == -1) return error.NotFound;
return std.math.cast(u15, idx) orelse error.OutOfRange;
}
pub fn finish(self: Results) void {
c.PQclear(self.result);
}
};
fn handleError(result: *c.PQresult) common.RowError {
const error_code = c.PQresultErrorField(result, c.PG_DIAG_SQLSTATE);
const state = errors.SqlState.parse(error_code) catch {
std.log.err("Database returned invalid error code {?s}", .{error_code});
return error.Unexpected;
};
const class = state.errorClass();
// TODO: This will crash if a value not defined in Postgres 14 is returned.
// See https://github.com/ziglang/zig/issues/12845
// If this issue does not get accepted we should redo this to use a comptime
// string map or something
std.log.err(
"Database returned error code {s}: Class {s} error {s}",
.{ error_code, @tagName(class), @tagName(state) },
);
return switch (class) {
.triggered_action_exception,
.feature_not_supported,
.invalid_transaction_initiation,
.locator_exception,
.cardinality_violation,
.data_exception,
.invalid_transaction_state,
.invalid_sql_statement_name,
.triggered_data_change_violation,
.dependent_privilege_descriptors_still_exist,
.invalid_transaction_termination,
.sql_routine_exception,
.invalid_cursor_name,
.external_routine_exception,
.external_routine_invocation_exception,
.savepoint_exception,
.invalid_catalog_name,
.invalid_schema_name,
.transaction_rollback, // TODO: consider deadlock avoidance/retry strategy
.snapshot_too_old,
.plpgsql_error,
=> error.SqlException,
.invalid_authorization_specification,
.invalid_grantor,
=> error.PermissionDenied,
.insufficient_resources,
.connection_exception,
.system_error,
.config_file_error,
.internal_error,
=> error.InternalException,
.operator_intervention => switch (state) {
.query_canceled => error.Cancelled,
else => error.InternalException,
},
.object_not_in_prerequisite_state => switch (state) {
.lock_not_available => error.DatabaseBusy,
else => error.SqlException,
},
.syntax_error_or_access_rule_violation => switch (state) {
.insufficient_privilege => error.PermissionDenied,
else => error.SqlException,
},
.with_check_option_violation,
.integrity_constraint_violation,
=> switch (state) {
.not_null_violation => error.NotNullViolation,
.foreign_key_violation => error.ForeignKeyViolation,
.unique_violation => error.UniqueViolation,
.check_violation => error.CheckViolation,
else => error.ConstraintViolation,
},
else => error.Unexpected,
};
}
pub const Row = struct {
result: *c.PGresult,
row_index: c_int,
pub fn get(self: Row, comptime T: type, idx: u16, alloc: ?Allocator) common.GetError!T {
const val = c.PQgetvalue(self.result, self.row_index, idx);
const is_null = (c.PQgetisnull(self.result, self.row_index, idx) != 0);
if (is_null) {
return if (@typeInfo(T) == .Optional) null else error.TypeMismatch;
}
if (val == null) return error.Unexpected;
const len = std.math.cast(
usize,
c.PQgetlength(self.result, self.row_index, idx),
) orelse return error.Unexpected;
return try common.parseValueNotNull(alloc, T, val[0..len]);
}
};
pub const Db = struct {
conn: *c.PGconn,
pub fn open(conn_str: [:0]const u8) common.OpenError!Db {
const conn = c.PQconnectdb(conn_str.ptr) orelse {
std.log.err("Unable to connect to database", .{});
return error.Unexpected;
};
errdefer c.PQfinish(conn);
std.log.info("Connecting to database using provided connection string...", .{});
switch (c.PQstatus(conn)) {
c.CONNECTION_OK => {},
c.CONNECTION_BAD => {
std.log.err("Error connecting to database: {?s}", .{c.PQerrorMessage(conn)});
return error.BadConnection;
},
else => |status| {
std.log.err("Unexpected PQstatus {}: {?s}", .{ status, c.PQerrorMessage(conn) });
return error.Unexpected;
},
}
std.log.info("DB connection established", .{});
return Db{ .conn = conn };
}
pub fn close(self: Db) void {
c.PQfinish(self.conn);
}
const format_text = 0;
const format_binary = 1;
pub fn exec(self: Db, sql: [:0]const u8, args: anytype, alloc: ?Allocator) !Results {
const result = blk: {
if (comptime args.len > 0) {
var arena = std.heap.ArenaAllocator.init(alloc orelse return error.AllocatorRequired);
defer arena.deinit();
const params = try arena.allocator().alloc(?[*:0]const u8, args.len);
inline for (args) |arg, i| params[i] = if (try common.prepareParamText(&arena, arg)) |slice| slice.ptr else null;
break :blk c.PQexecParams(self.conn, sql.ptr, @intCast(c_int, params.len), null, params.ptr, null, null, format_text);
} else {
break :blk c.PQexecParams(self.conn, sql.ptr, 0, null, null, null, null, format_text);
}
} orelse {
std.log.err("Error occurred in sql query: {?s}", .{c.PQerrorMessage(self.conn)});
return error.Unexpected;
};
errdefer c.PQclear(result);
const status = c.PQresultStatus(result);
switch (status) {
c.PGRES_COMMAND_OK,
c.PGRES_TUPLES_OK,
=> return Results{ .result = result },
c.PGRES_EMPTY_QUERY => return error.InvalidSql,
c.PGRES_BAD_RESPONSE => {
std.log.err("Database returned invalid response: {?s}", .{c.PQresultErrorMessage(result)});
return error.Database;
},
c.PGRES_FATAL_ERROR => return handleError(result),
else => |err| {
std.log.err(
"Unexpected PQresultStatus {?s} ({}): {?s}",
.{ c.PQresStatus(err), err, c.PQresultErrorMessage(result) },
);
return error.Unexpected;
},
}
}
};

View File

@ -0,0 +1,3 @@
usingnamespace @cImport({
@cInclude("libpq-fe.h");
});

View File

@ -0,0 +1,341 @@
const std = @import("std");
const c = @import("./c.zig");
const readIntBig = std.mem.readIntBig;
const Code = u40; // 8 * 5 = 40
const code_class_mask: Code = 0xFFFF_000000;
pub const SqlStateClass = blk: {
const info = @typeInfo(SqlState).Enum;
var fields = &[0]std.builtin.Type.EnumField{};
for (info.fields) |field| {
if (field.value & ~code_class_mask == 0) fields = fields ++ &.{field};
}
break :blk @Type(.{ .Enum = .{
.layout = info.layout,
.tag_type = Code,
.fields = fields,
.decls = &.{},
.is_exhaustive = false,
} });
};
// SqlState values for Postgres 14
pub const SqlState = enum(Code) {
pub const ParseError = error{ InvalidSize, NullPointer };
pub fn parse(code_str: [*c]const u8) ParseError!SqlState {
if (code_str == null) return error.NullPointer;
const slice = std.mem.span(code_str);
if (slice.len != @sizeOf(Code)) return error.InvalidSize;
return @intToEnum(SqlState, std.mem.readIntSliceBig(Code, slice));
}
pub fn errorClass(code: SqlState) SqlStateClass {
return @intToEnum(SqlStateClass, @enumToInt(code) & code_class_mask);
}
// Class 00 Successful Completion
successful_completion = readIntBig(Code, "00000"),
// Class 01 Warning
warning = readIntBig(Code, "01000"),
dynamic_result_sets_returned = readIntBig(Code, "0100C"),
implicit_zero_bit_padding = readIntBig(Code, "01008"),
null_value_eliminated_in_set_function = readIntBig(Code, "01003"),
privilege_not_granted = readIntBig(Code, "01007"),
privilege_not_revoked = readIntBig(Code, "01006"),
string_data_right_truncation = readIntBig(Code, "01004"),
deprecated_feature = readIntBig(Code, "01P01"),
// Class 02 No Data (this is also a warning class per the SQL standard)
no_data = readIntBig(Code, "02000"),
no_additional_dynamic_result_sets_returned = readIntBig(Code, "02001"),
// Class 03 SQL Statement Not Yet Complete
sql_statement_not_yet_complete = readIntBig(Code, "03000"),
// Class 08 Connection Exception
connection_exception = readIntBig(Code, "08000"),
connection_does_not_exist = readIntBig(Code, "08003"),
connection_failure = readIntBig(Code, "08006"),
sqlclient_unable_to_establish_sqlconnection = readIntBig(Code, "08001"),
sqlserver_rejected_establishment_of_sqlconnection = readIntBig(Code, "08004"),
transaction_resolution_unknown = readIntBig(Code, "08007"),
protocol_violation = readIntBig(Code, "08P01"),
// Class 09 Triggered Action Exception
triggered_action_exception = readIntBig(Code, "09000"),
// Class 0A Feature Not Supported
feature_not_supported = readIntBig(Code, "0A000"),
// Class 0B Invalid Transaction Initiation
invalid_transaction_initiation = readIntBig(Code, "0B000"),
// Class 0F Locator Exception
locator_exception = readIntBig(Code, "0F000"),
invalid_locator_specification = readIntBig(Code, "0F001"),
// Class 0L Invalid Grantor
invalid_grantor = readIntBig(Code, "0L000"),
invalid_grant_operation = readIntBig(Code, "0LP01"),
// Class 0P Invalid Role Specification
invalid_role_specification = readIntBig(Code, "0P000"),
// Class 0Z Diagnostics Exception
diagnostics_exception = readIntBig(Code, "0Z000"),
stacked_diagnostics_accessed_without_active_handler = readIntBig(Code, "0Z002"),
// Class 20 Case Not Found
case_not_found = readIntBig(Code, "20000"),
// Class 21 Cardinality Violation
cardinality_violation = readIntBig(Code, "21000"),
// Class 22 Data Exception
data_exception = readIntBig(Code, "22000"),
array_subscript_error = readIntBig(Code, "2202E"),
character_not_in_repertoire = readIntBig(Code, "22021"),
datetime_field_overflow = readIntBig(Code, "22008"),
division_by_zero = readIntBig(Code, "22012"),
error_in_assignment = readIntBig(Code, "22005"),
escape_character_conflict = readIntBig(Code, "2200B"),
indicator_overflow = readIntBig(Code, "22022"),
interval_field_overflow = readIntBig(Code, "22015"),
invalid_argument_for_logarithm = readIntBig(Code, "2201E"),
invalid_argument_for_ntile_function = readIntBig(Code, "22014"),
invalid_argument_for_nth_value_function = readIntBig(Code, "22016"),
invalid_argument_for_power_function = readIntBig(Code, "2201F"),
invalid_argument_for_width_bucket_function = readIntBig(Code, "2201G"),
invalid_character_value_for_cast = readIntBig(Code, "22018"),
invalid_datetime_format = readIntBig(Code, "22007"),
invalid_escape_character = readIntBig(Code, "22019"),
invalid_escape_octet = readIntBig(Code, "2200D"),
invalid_escape_sequence = readIntBig(Code, "22025"),
nonstandard_use_of_escape_character = readIntBig(Code, "22P06"),
invalid_indicator_parameter_value = readIntBig(Code, "22010"),
invalid_parameter_value = readIntBig(Code, "22023"),
invalid_preceding_or_following_size = readIntBig(Code, "22013"),
invalid_regular_expression = readIntBig(Code, "2201B"),
invalid_row_count_in_limit_clause = readIntBig(Code, "2201W"),
invalid_row_count_in_result_offset_clause = readIntBig(Code, "2201X"),
invalid_tablesample_argument = readIntBig(Code, "2202H"),
invalid_tablesample_repeat = readIntBig(Code, "2202G"),
invalid_time_zone_displacement_value = readIntBig(Code, "22009"),
invalid_use_of_escape_character = readIntBig(Code, "2200C"),
most_specific_type_mismatch = readIntBig(Code, "2200G"),
null_value_not_allowed = readIntBig(Code, "22004"),
null_value_no_indicator_parameter = readIntBig(Code, "22002"),
numeric_value_out_of_range = readIntBig(Code, "22003"),
sequence_generator_limit_exceeded = readIntBig(Code, "2200H"),
string_data_length_mismatch = readIntBig(Code, "22026"),
string_data_right_truncation = readIntBig(Code, "22001"),
substring_error = readIntBig(Code, "22011"),
trim_error = readIntBig(Code, "22027"),
unterminated_c_string = readIntBig(Code, "22024"),
zero_length_character_string = readIntBig(Code, "2200F"),
floating_point_exception = readIntBig(Code, "22P01"),
invalid_text_representation = readIntBig(Code, "22P02"),
invalid_binary_representation = readIntBig(Code, "22P03"),
bad_copy_file_format = readIntBig(Code, "22P04"),
untranslatable_character = readIntBig(Code, "22P05"),
not_an_xml_document = readIntBig(Code, "2200L"),
invalid_xml_document = readIntBig(Code, "2200M"),
invalid_xml_content = readIntBig(Code, "2200N"),
invalid_xml_comment = readIntBig(Code, "2200S"),
invalid_xml_processing_instruction = readIntBig(Code, "2200T"),
duplicate_json_object_key_value = readIntBig(Code, "22030"),
invalid_argument_for_sql_json_datetime_function = readIntBig(Code, "22031"),
invalid_json_text = readIntBig(Code, "22032"),
invalid_sql_json_subscript = readIntBig(Code, "22033"),
more_than_one_sql_json_item = readIntBig(Code, "22034"),
no_sql_json_item = readIntBig(Code, "22035"),
non_numeric_sql_json_item = readIntBig(Code, "22036"),
non_unique_keys_in_a_json_object = readIntBig(Code, "22037"),
singleton_sql_json_item_required = readIntBig(Code, "22038"),
sql_json_array_not_found = readIntBig(Code, "22039"),
sql_json_member_not_found = readIntBig(Code, "2203A"),
sql_json_number_not_found = readIntBig(Code, "2203B"),
sql_json_object_not_found = readIntBig(Code, "2203C"),
too_many_json_array_elements = readIntBig(Code, "2203D"),
too_many_json_object_members = readIntBig(Code, "2203E"),
sql_json_scalar_required = readIntBig(Code, "2203F"),
// Class 23 Integrity Constraint Violation
integrity_constraint_violation = readIntBig(Code, "23000"),
restrict_violation = readIntBig(Code, "23001"),
not_null_violation = readIntBig(Code, "23502"),
foreign_key_violation = readIntBig(Code, "23503"),
unique_violation = readIntBig(Code, "23505"),
check_violation = readIntBig(Code, "23514"),
exclusion_violation = readIntBig(Code, "23P01"),
// Class 24 Invalid Cursor State
invalid_cursor_state = readIntBig(Code, "24000"),
// Class 25 Invalid Transaction State
invalid_transaction_state = readIntBig(Code, "25000"),
active_sql_transaction = readIntBig(Code, "25001"),
branch_transaction_already_active = readIntBig(Code, "25002"),
held_cursor_requires_same_isolation_level = readIntBig(Code, "25008"),
inappropriate_access_mode_for_branch_transaction = readIntBig(Code, "25003"),
inappropriate_isolation_level_for_branch_transaction = readIntBig(Code, "25004"),
no_active_sql_transaction_for_branch_transaction = readIntBig(Code, "25005"),
read_only_sql_transaction = readIntBig(Code, "25006"),
schema_and_data_statement_mixing_not_supported = readIntBig(Code, "25007"),
no_active_sql_transaction = readIntBig(Code, "25P01"),
in_failed_sql_transaction = readIntBig(Code, "25P02"),
idle_in_transaction_session_timeout = readIntBig(Code, "25P03"),
// Class 26 Invalid SQL Statement Name
invalid_sql_statement_name = readIntBig(Code, "26000"),
// Class 27 Triggered Data Change Violation
triggered_data_change_violation = readIntBig(Code, "27000"),
// Class 28 Invalid Authorization Specification
invalid_authorization_specification = readIntBig(Code, "28000"),
invalid_password = readIntBig(Code, "28P01"),
// Class 2B Dependent Privilege Descriptors Still Exist
dependent_privilege_descriptors_still_exist = readIntBig(Code, "2B000"),
dependent_objects_still_exist = readIntBig(Code, "2BP01"),
// Class 2D Invalid Transaction Termination
invalid_transaction_termination = readIntBig(Code, "2D000"),
// Class 2F SQL Routine Exception
sql_routine_exception = readIntBig(Code, "2F000"),
function_executed_no_return_statement = readIntBig(Code, "2F005"),
modifying_sql_data_not_permitted = readIntBig(Code, "2F002"),
prohibited_sql_statement_attempted = readIntBig(Code, "2F003"),
reading_sql_data_not_permitted = readIntBig(Code, "2F004"),
// Class 34 Invalid Cursor Name
invalid_cursor_name = readIntBig(Code, "34000"),
// Class 38 External Routine Exception
external_routine_exception = readIntBig(Code, "38000"),
containing_sql_not_permitted = readIntBig(Code, "38001"),
modifying_sql_data_not_permitted = readIntBig(Code, "38002"),
prohibited_sql_statement_attempted = readIntBig(Code, "38003"),
reading_sql_data_not_permitted = readIntBig(Code, "38004"),
// Class 39 External Routine Invocation Exception
external_routine_invocation_exception = readIntBig(Code, "39000"),
invalid_sqlstate_returned = readIntBig(Code, "39001"),
null_value_not_allowed = readIntBig(Code, "39004"),
trigger_protocol_violated = readIntBig(Code, "39P01"),
srf_protocol_violated = readIntBig(Code, "39P02"),
event_trigger_protocol_violated = readIntBig(Code, "39P03"),
// Class 3B Savepoint Exception
savepoint_exception = readIntBig(Code, "3B000"),
invalid_savepoint_specification = readIntBig(Code, "3B001"),
// Class 3D Invalid Catalog Name
invalid_catalog_name = readIntBig(Code, "3D000"),
// Class 3F Invalid Schema Name
invalid_schema_name = readIntBig(Code, "3F000"),
// Class 40 Transaction Rollback
transaction_rollback = readIntBig(Code, "40000"),
transaction_integrity_constraint_violation = readIntBig(Code, "40002"),
serialization_failure = readIntBig(Code, "40001"),
statement_completion_unknown = readIntBig(Code, "40003"),
deadlock_detected = readIntBig(Code, "40P01"),
// Class 42 Syntax Error or Access Rule Violation
syntax_error_or_access_rule_violation = readIntBig(Code, "42000"),
syntax_error = readIntBig(Code, "42601"),
insufficient_privilege = readIntBig(Code, "42501"),
cannot_coerce = readIntBig(Code, "42846"),
grouping_error = readIntBig(Code, "42803"),
windowing_error = readIntBig(Code, "42P20"),
invalid_recursion = readIntBig(Code, "42P19"),
invalid_foreign_key = readIntBig(Code, "42830"),
invalid_name = readIntBig(Code, "42602"),
name_too_long = readIntBig(Code, "42622"),
reserved_name = readIntBig(Code, "42939"),
datatype_mismatch = readIntBig(Code, "42804"),
indeterminate_datatype = readIntBig(Code, "42P18"),
collation_mismatch = readIntBig(Code, "42P21"),
indeterminate_collation = readIntBig(Code, "42P22"),
wrong_object_type = readIntBig(Code, "42809"),
generated_always = readIntBig(Code, "428C9"),
undefined_column = readIntBig(Code, "42703"),
undefined_function = readIntBig(Code, "42883"),
undefined_table = readIntBig(Code, "42P01"),
undefined_parameter = readIntBig(Code, "42P02"),
undefined_object = readIntBig(Code, "42704"),
duplicate_column = readIntBig(Code, "42701"),
duplicate_cursor = readIntBig(Code, "42P03"),
duplicate_database = readIntBig(Code, "42P04"),
duplicate_function = readIntBig(Code, "42723"),
duplicate_prepared_statement = readIntBig(Code, "42P05"),
duplicate_schema = readIntBig(Code, "42P06"),
duplicate_table = readIntBig(Code, "42P07"),
duplicate_alias = readIntBig(Code, "42712"),
duplicate_object = readIntBig(Code, "42710"),
ambiguous_column = readIntBig(Code, "42702"),
ambiguous_function = readIntBig(Code, "42725"),
ambiguous_parameter = readIntBig(Code, "42P08"),
ambiguous_alias = readIntBig(Code, "42P09"),
invalid_column_reference = readIntBig(Code, "42P10"),
invalid_column_definition = readIntBig(Code, "42611"),
invalid_cursor_definition = readIntBig(Code, "42P11"),
invalid_database_definition = readIntBig(Code, "42P12"),
invalid_function_definition = readIntBig(Code, "42P13"),
invalid_prepared_statement_definition = readIntBig(Code, "42P14"),
invalid_schema_definition = readIntBig(Code, "42P15"),
invalid_table_definition = readIntBig(Code, "42P16"),
invalid_object_definition = readIntBig(Code, "42P17"),
// Class 44 WITH CHECK OPTION Violation
with_check_option_violation = readIntBig(Code, "44000"),
// Class 53 Insufficient Resources
insufficient_resources = readIntBig(Code, "53000"),
disk_full = readIntBig(Code, "53100"),
out_of_memory = readIntBig(Code, "53200"),
too_many_connections = readIntBig(Code, "53300"),
configuration_limit_exceeded = readIntBig(Code, "53400"),
// Class 54 Program Limit Exceeded
program_limit_exceeded = readIntBig(Code, "54000"),
statement_too_complex = readIntBig(Code, "54001"),
too_many_columns = readIntBig(Code, "54011"),
too_many_arguments = readIntBig(Code, "54023"),
// Class 55 Object Not In Prerequisite State
object_not_in_prerequisite_state = readIntBig(Code, "55000"),
object_in_use = readIntBig(Code, "55006"),
cant_change_runtime_param = readIntBig(Code, "55P02"),
lock_not_available = readIntBig(Code, "55P03"),
unsafe_new_enum_value_usage = readIntBig(Code, "55P04"),
// Class 57 Operator Intervention
operator_intervention = readIntBig(Code, "57000"),
query_canceled = readIntBig(Code, "57014"),
admin_shutdown = readIntBig(Code, "57P01"),
crash_shutdown = readIntBig(Code, "57P02"),
cannot_connect_now = readIntBig(Code, "57P03"),
database_dropped = readIntBig(Code, "57P04"),
idle_session_timeout = readIntBig(Code, "57P05"),
// Class 58 System Error (errors external to PostgreSQL itself)
system_error = readIntBig(Code, "58000"),
io_error = readIntBig(Code, "58030"),
undefined_file = readIntBig(Code, "58P01"),
duplicate_file = readIntBig(Code, "58P02"),
// Class 72 Snapshot Failure
snapshot_too_old = readIntBig(Code, "72000"),
// Class F0 Configuration File Error
config_file_error = readIntBig(Code, "F0000"),
lock_file_exists = readIntBig(Code, "F0001"),
// Class HV Foreign Data Wrapper Error (SQL/MED)
fdw_error = readIntBig(Code, "HV000"),
fdw_column_name_not_found = readIntBig(Code, "HV005"),
fdw_dynamic_parameter_value_needed = readIntBig(Code, "HV002"),
fdw_function_sequence_error = readIntBig(Code, "HV010"),
fdw_inconsistent_descriptor_information = readIntBig(Code, "HV021"),
fdw_invalid_attribute_value = readIntBig(Code, "HV024"),
fdw_invalid_column_name = readIntBig(Code, "HV007"),
fdw_invalid_column_number = readIntBig(Code, "HV008"),
fdw_invalid_data_type = readIntBig(Code, "HV004"),
fdw_invalid_data_type_descriptors = readIntBig(Code, "HV006"),
fdw_invalid_descriptor_field_identifier = readIntBig(Code, "HV091"),
fdw_invalid_handle = readIntBig(Code, "HV00B"),
fdw_invalid_option_index = readIntBig(Code, "HV00C"),
fdw_invalid_option_name = readIntBig(Code, "HV00D"),
fdw_invalid_string_length_or_buffer_length = readIntBig(Code, "HV090"),
fdw_invalid_string_format = readIntBig(Code, "HV00A"),
fdw_invalid_use_of_null_pointer = readIntBig(Code, "HV009"),
fdw_too_many_handles = readIntBig(Code, "HV014"),
fdw_out_of_memory = readIntBig(Code, "HV001"),
fdw_no_schemas = readIntBig(Code, "HV00P"),
fdw_option_name_not_found = readIntBig(Code, "HV00J"),
fdw_reply_handle = readIntBig(Code, "HV00K"),
fdw_schema_not_found = readIntBig(Code, "HV00Q"),
fdw_table_not_found = readIntBig(Code, "HV00R"),
fdw_unable_to_create_execution = readIntBig(Code, "HV00L"),
fdw_unable_to_create_reply = readIntBig(Code, "HV00M"),
fdw_unable_to_establish_connection = readIntBig(Code, "HV00N"),
// Class P0 PL/pgSQL Error
plpgsql_error = readIntBig(Code, "P0000"),
raise_exception = readIntBig(Code, "P0001"),
no_data_found = readIntBig(Code, "P0002"),
too_many_rows = readIntBig(Code, "P0003"),
assert_failure = readIntBig(Code, "P0004"),
// Class XX Internal Error
internal_error = readIntBig(Code, "XX000"),
data_corrupted = readIntBig(Code, "XX001"),
index_corrupted = readIntBig(Code, "XX002"),
_,
};

View File

@ -9,19 +9,6 @@ const Uuid = util.Uuid;
const DateTime = util.DateTime;
const Allocator = std.mem.Allocator;
const UnexpectedError = error{Unexpected};
pub const OpenError = error{
DbCorrupt,
BadPathName,
IsDir,
InputOutput,
NoSpaceLeft,
} || UnexpectedError;
pub const PrepareError = UnexpectedError;
pub const RowGetError = error{ InvalidData, StreamTooLong, OutOfMemory } || UnexpectedError;
pub const BindError = UnexpectedError;
fn getCharPos(text: []const u8, offset: c_int) struct { row: usize, col: usize } {
var row: usize = 0;
var col: usize = 0;
@ -41,7 +28,7 @@ fn getCharPos(text: []const u8, offset: c_int) struct { row: usize, col: usize }
return .{ .row = row, .col = col };
}
fn handleUnexpectedError(db: *c.sqlite3, code: c_int, sql_text: ?[]const u8) UnexpectedError {
fn handleUnexpectedError(db: *c.sqlite3, code: c_int, sql_text: ?[]const u8) anyerror {
std.log.err("Unexpected error in SQLite engine: {s} ({})", .{ c.sqlite3_errstr(code), code });
std.log.debug("Additional details:", .{});
@ -61,30 +48,36 @@ fn handleUnexpectedError(db: *c.sqlite3, code: c_int, sql_text: ?[]const u8) Une
pub const Db = struct {
db: *c.sqlite3,
pub fn open(path: [:0]const u8) OpenError!Db {
pub fn open(path: [:0]const u8) common.OpenError!Db {
const flags = c.SQLITE_OPEN_READWRITE | c.SQLITE_OPEN_CREATE | c.SQLITE_OPEN_EXRESCODE;
var db: ?*c.sqlite3 = undefined;
var db: [*c]c.sqlite3 = null;
switch (c.sqlite3_open_v2(@ptrCast([*c]const u8, path), &db, flags, null)) {
c.SQLITE_OK => {},
c.SQLITE_NOTADB, c.SQLITE_CORRUPT => return error.DbCorrupt,
c.SQLITE_IOERR_WRITE, c.SQLITE_IOERR_READ => return error.InputOutput,
c.SQLITE_CANTOPEN_ISDIR => return error.IsDir,
c.SQLITE_CANTOPEN_FULLPATH => return error.BadPathName,
c.SQLITE_FULL => return error.NoSpaceLeft,
else => |code| {
if (db == null) {
// this path should only be hit if out of memory, but log it anyways
std.log.err(
"Unable to open SQLite DB \"{s}\". Error: {?s} ({})",
.{ path, c.sqlite3_errstr(code), code },
);
return error.InternalException;
}
else => |err| {
const ext_code = c.sqlite3_extended_errcode(db);
std.log.err(
\\Unable to open SQLite DB "{s}"
\\Error: {s} ({})
, .{ path, c.sqlite3_errstr(err), err });
\\Unable to open SQLite DB "{s}". Error: {?s} ({})
\\Details: {?s}
,
.{ path, c.sqlite3_errstr(ext_code), ext_code, c.sqlite3_errmsg(db) },
);
return error.Unexpected;
},
}
return Db{
.db = db.?,
.db = db,
};
}
@ -103,7 +96,7 @@ pub const Db = struct {
}
}
pub fn exec(self: Db, sql: []const u8, args: anytype, opts: common.QueryOptions) !Results {
pub fn exec(self: Db, sql: []const u8, args: anytype, opts: common.QueryOptions) common.ExecError!Results {
var stmt: ?*c.sqlite3_stmt = undefined;
switch (c.sqlite3_prepare_v2(self.db, sql.ptr, @intCast(c_int, sql.len), &stmt, null)) {
c.SQLITE_OK => {},
@ -134,7 +127,7 @@ pub const Db = struct {
return handleUnexpectedError(self.db, err, sql);
},
}
} else if (!opts.ignore_unknown_parameters) return error.UnknownParameter;
} else if (!opts.ignore_unknown_parameters) return error.UndefinedParameter;
}
return Results{ .stmt = stmt.?, .db = self.db };
@ -179,8 +172,7 @@ pub const Results = struct {
}
}
pub const StepError = UnexpectedError;
pub fn row(self: Results) !?Row {
pub fn row(self: Results) common.RowError!?Row {
return switch (c.sqlite3_step(self.stmt)) {
c.SQLITE_ROW => Row{ .stmt = self.stmt, .db = self.db },
c.SQLITE_DONE => null,
@ -193,18 +185,18 @@ pub const Results = struct {
return ptr[0..std.mem.len(ptr)];
}
pub fn columnCount(self: Results) u15 {
pub fn columnCount(self: Results) common.ColumnCountError!u15 {
return @intCast(u15, c.sqlite3_column_count(self.stmt));
}
pub fn columnName(self: Results, idx: u15) ![]const u8 {
fn columnName(self: Results, idx: u15) ![]const u8 {
return if (c.sqlite3_column_name(self.stmt, idx)) |ptr|
ptr[0..std.mem.len(ptr)]
else
return error.OutOfMemory;
return error.Unexpected;
}
pub fn columnNameToIndex(self: Results, name: []const u8) !u15 {
pub fn columnIndex(self: Results, name: []const u8) common.ColumnIndexError!u15 {
var i: u15 = 0;
const count = self.columnCount();
while (i < count) : (i += 1) {
@ -212,7 +204,7 @@ pub const Results = struct {
if (std.mem.eql(u8, name, column)) return i;
}
return error.ColumnNotFound;
return error.NotFound;
}
};
@ -220,15 +212,15 @@ pub const Row = struct {
stmt: *c.sqlite3_stmt,
db: *c.sqlite3,
pub fn get(self: Row, comptime T: type, idx: u15, alloc: ?Allocator) !T {
pub fn get(self: Row, comptime T: type, idx: u15, alloc: ?Allocator) common.GetError!T {
if (c.sqlite3_column_type(self.stmt, idx) == c.SQLITE_NULL) {
return if (@typeInfo(T) == .Optional) null else error.NullValue;
return if (@typeInfo(T) == .Optional) null else error.TypeMismatch;
}
return self.getNotNull(T, idx, alloc);
}
fn getNotNull(self: Row, comptime T: type, idx: u15, alloc: ?Allocator) !T {
fn getNotNull(self: Row, comptime T: type, idx: u15, alloc: ?Allocator) error.GetError!T {
return switch (T) {
f32, f64 => @floatCast(T, c.sqlite3_column_double(self.stmt, idx)),
@ -243,7 +235,7 @@ pub const Row = struct {
};
}
fn getFromString(self: Row, comptime T: type, idx: u15, alloc: ?Allocator) !T {
fn getFromString(self: Row, comptime T: type, idx: u15, alloc: ?Allocator) error.GetError!T {
const ptr = c.sqlite3_column_text(self.stmt, idx);
const size = @intCast(usize, c.sqlite3_column_bytes(self.stmt, idx));
const str = ptr[0..size];

105
src/sql/errors.zig Normal file
View File

@ -0,0 +1,105 @@
const std = @import("std");
// Error for any miscellaneous or unexpected error codes
const UnexpectedError = error{Unexpected};
// Errors related to database connections, common to (most) api calls
const ConnectionError = error{
// The operation was cancelled by the user
Cancelled,
// The database connection could not be created or was lost
BadConnection,
// An internal error occurred in the engine.
// Examples include:
// - Out of Memory
// - Filesystem full
// - Unknown crash
// - Filesystem permissions denied
InternalError,
};
// Errors related to constraint validation
const ConstraintError = error{
// A `NOT NULL` constraint was violated
NotNullViolation,
// A `FOREIGN KEY` constraint was violated
ForeignKeyViolation,
// A `UNIQUE` constraint was violated
UniqueViolation,
// A `CHECK` constraint was violated
CheckViolation,
// A unknown constraint type was violated
ConstraintViolation,
};
// Errors related to argument binding
const ArgumentError = error{
// One of the arguments passed could not be marshalled to pass to the SQL engine
InvalidArgument,
// The set of arguments passed did not map to query parameters
UndefinedParameter,
// The allocator used for staging the query ran out of memory
OutOfMemory,
};
// Errors related to retrieving query result columns
const ResultColumnError = error{
// The allocator used for retrieving the results ran out of memory
OutOfMemory,
// A type error occurred when parsing results (means invalid data is in the DB)
ResultTypeMismatch,
};
// Errors related to executing SQL queries
const StartQueryError = error{
// The database is locked by another query and the timeout was exceeded
DatabaseBusy,
// Access to one or more resources was denied
PermissionDenied,
// The SQL query had invalid syntax or used an invalid identifier
InvalidSql,
// A type error occurred during the query (means query is written wrong)
QueryTypeMismatch,
// The set of columns to parse did not match the columns returned by the query
ColumnMismatch,
// Either an explicit transaction was open and a query method was called on the DB directly,
// or no explicit transaction was open and a query method was called on a transaction;
BadTransactionState,
};
const RowCountError = error{
NoRows,
TooManyRows,
};
pub const OpenError = error{
BadConnection,
InternalError,
};
pub const library_errors = struct {
const BaseError = ConnectionError || UnexpectedError;
// TODO: clean this up
pub const OpenError = BaseError;
pub const QueryError = BaseError || ArgumentError || ConstraintError || StartQueryError;
pub const RowError = BaseError || ResultColumnError || ConstraintError || StartQueryError;
pub const QueryRowError = QueryError || RowError || RowCountError;
pub const ExecError = QueryError || RowCountError;
pub const BeginError = BaseError || StartQueryError;
pub const CommitError = BaseError || StartQueryError || ConstraintError;
};

View File

@ -1,11 +1,20 @@
const std = @import("std");
const util = @import("util");
const postgres = @import("./postgres.zig");
const sqlite = @import("./sqlite.zig");
const common = @import("./common.zig");
const postgres = @import("./engines/postgres.zig");
const sqlite = @import("./engines/sqlite.zig");
const common = @import("./engines/common.zig");
const Allocator = std.mem.Allocator;
const errors = @import("./errors.zig").library_errors;
pub const OpenError = errors.OpenError;
pub const QueryError = errors.QueryError;
pub const RowError = errors.RowError;
pub const QueryRowError = errors.QueryRowError;
pub const BeginError = errors.BeginError;
pub const CommitError = errors.CommitError;
pub const QueryOptions = common.QueryOptions;
pub const Engine = enum {
@ -22,11 +31,6 @@ pub const Config = union(Engine) {
},
};
pub const QueryError = error{
OutOfMemory,
ConnectionLost,
};
pub fn fieldList(comptime RowType: type) []const u8 {
comptime {
const fields = std.meta.fieldNames(RowType);
@ -76,14 +80,14 @@ const RawResults = union(Engine) {
};
}
fn columnNameToIndex(self: RawResults, name: []const u8) !u15 {
fn columnIndex(self: RawResults, name: []const u8) QueryError!u15 {
return try switch (self) {
.postgres => |pg| pg.columnNameToIndex(name),
.sqlite => |lite| lite.columnNameToIndex(name),
.postgres => |pg| pg.columnIndex(name),
.sqlite => |lite| lite.columnIndex(name),
};
}
fn row(self: *RawResults) !?Row {
fn row(self: *RawResults) RowError!?Row {
return switch (self.*) {
.postgres => |*pg| if (try pg.row()) |r| Row{ .postgres = r } else null,
.sqlite => |*lite| if (try lite.row()) |r| Row{ .sqlite = r } else null,
@ -103,11 +107,16 @@ pub fn Results(comptime T: type) type {
underlying: RawResults,
column_indices: [fields.len]u15,
fn from(underlying: RawResults) !Self {
fn from(underlying: RawResults) QueryError!Self {
if (std.debug.runtime_safety and std.meta.trait.isTuple(T) and fields.len != underlying.columnCount()) {
std.log.err("Expected {} columns in result, got {}", .{ fields.len, underlying.columnCount() });
return error.ColumnMismatch;
}
return Self{ .underlying = underlying, .column_indices = blk: {
var indices: [fields.len]u15 = undefined;
inline for (fields) |f, i| {
indices[i] = if (!std.meta.trait.isTuple(T)) try underlying.columnNameToIndex(f.name) else i;
indices[i] = if (!std.meta.trait.isTuple(T)) try underlying.columnIndex(f.name) else i;
}
break :blk indices;
} };
@ -126,7 +135,7 @@ pub fn Results(comptime T: type) type {
// Returns the next row of results, or null if there are no more rows.
// Caller owns all memory allocated. The entire object can be deallocated with a
// call to util.deepFree
pub fn row(self: *Self, alloc: ?Allocator) !?T {
pub fn row(self: *Self, alloc: ?Allocator) RowError!?T {
if (try self.underlying.row()) |row_val| {
var result: T = undefined;
var fields_allocated: usize = 0;
@ -157,7 +166,7 @@ const Row = union(Engine) {
// Not all types require an allocator to be present. If an allocator is needed but
// not required, it will return error.AllocatorRequired.
// The caller is responsible for deallocating T, if relevant.
fn get(self: Row, comptime T: type, idx: u15, alloc: ?Allocator) anyerror!T {
fn get(self: Row, comptime T: type, idx: u15, alloc: ?Allocator) !T {
return switch (self) {
.postgres => |pg| pg.get(T, idx, alloc),
.sqlite => |lite| lite.get(T, idx, alloc),
@ -165,170 +174,64 @@ const Row = union(Engine) {
}
};
const DbUnion = union(Engine) {
const QueryHelper = union(Engine) {
postgres: postgres.Db,
sqlite: sqlite.Db,
};
pub const ConstraintMode = enum {
deferred,
immediate,
};
pub const Db = struct {
tx_open: bool = false,
underlying: DbUnion,
pub fn open(cfg: Config) !Db {
return switch (cfg) {
.postgres => |postgres_cfg| Db{
.underlying = .{
.postgres = try postgres.Db.open(postgres_cfg.pg_conn_str),
},
},
.sqlite => |lite_cfg| Db{
.underlying = .{
.sqlite = try sqlite.Db.open(lite_cfg.sqlite_file_path),
},
},
};
}
pub fn close(self: *Db) void {
switch (self.underlying) {
.postgres => |pg| pg.close(),
.sqlite => |lite| lite.close(),
}
}
pub fn queryWithOptions(
self: *Db,
comptime RowType: type,
sql: [:0]const u8,
args: anytype,
opt: QueryOptions,
) !Results(RowType) {
if (self.tx_open) return error.TransactionOpen;
// Create fake transaction to use its functions
return (Tx{ .db = self }).queryWithOptions(RowType, sql, args, opt);
}
pub fn query(
self: *Db,
comptime RowType: type,
sql: [:0]const u8,
args: anytype,
alloc: ?Allocator,
) !Results(RowType) {
if (self.tx_open) return error.TransactionOpen;
// Create fake transaction to use its functions
return (Tx{ .db = self }).query(RowType, sql, args, alloc);
}
pub fn exec(
self: *Db,
sql: [:0]const u8,
args: anytype,
alloc: ?Allocator,
) !void {
if (self.tx_open) return error.TransactionOpen;
// Create fake transaction to use its functions
return (Tx{ .db = self }).exec(sql, args, alloc);
}
pub fn queryRow(
self: *Db,
comptime RowType: type,
sql: [:0]const u8,
args: anytype,
alloc: ?Allocator,
) !?RowType {
if (self.tx_open) return error.TransactionOpen;
// Create fake transaction to use its functions
return (Tx{ .db = self }).queryRow(RowType, sql, args, alloc);
}
pub fn insert(
self: *Db,
comptime table: []const u8,
value: anytype,
) !void {
if (self.tx_open) return error.TransactionOpen;
// Create fake transaction to use its functions
return (Tx{ .db = self }).insert(table, value);
}
pub fn sqlEngine(self: *Db) Engine {
return self.underlying;
}
// Begins a transaction
pub fn begin(self: *Db) !Tx {
const tx = Tx{ .db = self };
try tx.exec("BEGIN", .{}, null);
return tx;
}
};
pub const Tx = struct {
db: *Db,
// internal helper fn
fn queryInternal(
self: Tx,
self: QueryHelper,
sql: [:0]const u8,
args: anytype,
opt: QueryOptions,
) !RawResults {
return switch (self.db.underlying) {
return switch (self) {
.postgres => |pg| RawResults{ .postgres = try pg.exec(sql, args, opt.prep_allocator) },
.sqlite => |lite| RawResults{ .sqlite = try lite.exec(sql, args, opt) },
};
}
pub fn queryWithOptions(
self: Tx,
fn queryWithOptions(
self: QueryHelper,
comptime RowType: type,
sql: [:0]const u8,
args: anytype,
options: QueryOptions,
) !Results(RowType) {
) QueryError!Results(RowType) {
return Results(RowType).from(try self.queryInternal(sql, args, options));
}
// Executes a query and returns the result set
pub fn query(
self: Tx,
fn query(
self: QueryHelper,
comptime RowType: type,
sql: [:0]const u8,
args: anytype,
alloc: ?Allocator,
) !Results(RowType) {
) QueryError!Results(RowType) {
return self.queryWithOptions(RowType, sql, args, .{ .prep_allocator = alloc });
}
// Executes a query without returning results
pub fn exec(
self: Tx,
fn exec(
self: QueryHelper,
sql: [:0]const u8,
args: anytype,
alloc: ?Allocator,
) !void {
_ = try self.queryRow(std.meta.Tuple(&.{}), sql, args, alloc);
) QueryError!void {
try self.queryRow(void, sql, args, alloc);
}
// Runs a query and returns a single row
pub fn queryRow(
self: Tx,
fn queryRow(
self: QueryHelper,
comptime RowType: type,
q: [:0]const u8,
args: anytype,
alloc: ?Allocator,
) !?RowType {
) QueryRowError!?RowType {
var results = try self.query(RowType, q, args, alloc);
defer results.finish();
@compileLog(args);
const row = (try results.row(alloc)) orelse return null;
errdefer util.deepFree(alloc, row);
@ -344,8 +247,8 @@ pub const Tx = struct {
}
// Inserts a single value into a table
pub fn insert(
self: Tx,
fn insert(
self: QueryHelper,
comptime table: []const u8,
value: anytype,
) !void {
@ -373,19 +276,179 @@ pub const Tx = struct {
}
try self.exec(q, args_tuple, null);
}
};
pub fn sqlEngine(self: Tx) Engine {
return self.db.underlying;
pub const ConstraintMode = enum {
deferred,
immediate,
};
pub const Db = struct {
tx_open: bool = false,
engine: QueryHelper,
pub fn open(cfg: Config) OpenError!Db {
return switch (cfg) {
.postgres => |postgres_cfg| Db{
.engine = .{
.postgres = try postgres.Db.open(postgres_cfg.pg_conn_str),
},
},
.sqlite => |lite_cfg| Db{
.engine = .{
.sqlite = try sqlite.Db.open(lite_cfg.sqlite_file_path),
},
},
};
}
pub fn setConstraintMode(self: Tx, mode: ConstraintMode) !void {
switch (self.db.underlying) {
pub fn close(self: *Db) void {
switch (self.engine) {
.postgres => |pg| pg.close(),
.sqlite => |lite| lite.close(),
}
}
pub fn queryWithOptions(
self: *Db,
comptime RowType: type,
sql: [:0]const u8,
args: anytype,
opt: QueryOptions,
) QueryError!Results(RowType) {
if (self.tx_open) return error.BadTransactionState;
return self.engine.queryWithOptions(RowType, sql, args, opt);
}
pub fn query(
self: *Db,
comptime RowType: type,
sql: [:0]const u8,
args: anytype,
alloc: ?Allocator,
) QueryError!Results(RowType) {
if (self.tx_open) return error.BadTransactionState;
return self.engine.query(RowType, sql, args, alloc);
}
pub fn exec(
self: *Db,
sql: [:0]const u8,
args: anytype,
alloc: ?Allocator,
) QueryError!void {
if (self.tx_open) return error.BadTransactionState;
return self.engine.exec(sql, args, alloc);
}
pub fn queryRow(
self: *Db,
comptime RowType: type,
sql: [:0]const u8,
args: anytype,
alloc: ?Allocator,
) QueryRowError!?RowType {
if (self.tx_open) return error.BadTransactionState;
return self.engine.queryRow(RowType, sql, args, alloc);
}
pub fn insert(
self: *Db,
comptime table: []const u8,
value: anytype,
) !void {
if (self.tx_open) return error.BadTransactionState;
return self.engine.insert(table, value);
}
pub fn sqlEngine(self: *Db) Engine {
return self.engine;
}
// Begins a transaction
pub fn begin(self: *Db) !Tx {
if (self.tx_open) return error.BadTransactionState;
const tx = Tx{ .db = self };
try tx.exec("BEGIN", {}, null);
self.tx_open = true;
return tx;
}
};
pub const Tx = struct {
db: *Db,
pub fn queryWithOptions(
self: Tx,
comptime RowType: type,
sql: [:0]const u8,
args: anytype,
options: QueryOptions,
) QueryError!Results(RowType) {
if (!self.db.tx_open) return error.BadTransactionState;
return self.db.engine.queryWithOptions(RowType, sql, args, options);
}
// Executes a query and returns the result set
pub fn query(
self: Tx,
comptime RowType: type,
sql: [:0]const u8,
args: anytype,
alloc: ?Allocator,
) QueryError!Results(RowType) {
if (!self.db.tx_open) return error.BadTransactionState;
return self.db.engine.query(RowType, sql, args, alloc);
}
// Executes a query without returning results
pub fn exec(
self: Tx,
sql: [:0]const u8,
args: anytype,
alloc: ?Allocator,
) QueryError!void {
if (!self.db.tx_open) return error.BadTransactionState;
return self.db.engine.exec(sql, args, alloc);
}
// Runs a query and returns a single row
pub fn queryRow(
self: Tx,
comptime RowType: type,
sql: [:0]const u8,
args: anytype,
alloc: ?Allocator,
) QueryRowError!?RowType {
if (!self.db.tx_open) return error.BadTransactionState;
return self.db.engine.queryRow(RowType, sql, args, alloc);
}
// Inserts a single value into a table
pub fn insert(
self: Tx,
comptime table: []const u8,
value: anytype,
) !void {
if (!self.db.tx_open) return error.BadTransactionState;
return self.db.engine.insert(table, value);
}
pub fn sqlEngine(self: Tx) Engine {
return self.db.engine;
}
pub fn setConstraintMode(self: Tx, mode: ConstraintMode) QueryError!void {
if (!self.db.tx_open) return error.BadTransactionState;
switch (self.db.engine) {
.sqlite => try self.exec(
switch (mode) {
.immediate => "PRAGMA defer_foreign_keys = FALSE",
.deferred => "PRAGMA defer_foreign_keys = TRUE",
},
.{},
{},
null,
),
.postgres => try self.exec(
@ -393,19 +456,23 @@ pub const Tx = struct {
.immediate => "SET CONSTRAINTS ALL IMMEDIATE",
.deferred => "SET CONSTRAINTS ALL DEFERRED",
},
.{},
{},
null,
),
}
}
pub fn rollback(self: Tx) void {
self.exec("ROLLBACK", .{}, null) catch |err| {
if (!self.db.tx_open) @panic("Transaction not open");
self.exec("ROLLBACK", {}, null) catch |err| {
std.log.err("Error occured during rollback operation: {}", .{err});
};
self.db.tx_open = false;
}
pub fn commit(self: Tx) !void {
try self.exec("COMMIT", .{}, null);
pub fn commit(self: Tx) CommitError!void {
if (!self.db.tx_open) return error.BadTransactionState;
try self.exec("COMMIT", {}, null);
self.db.tx_open = false;
}
};

View File

@ -1,134 +0,0 @@
const std = @import("std");
const util = @import("util");
const common = @import("./common.zig");
const c = @cImport({
@cInclude("libpq-fe.h");
});
const Allocator = std.mem.Allocator;
pub const Results = struct {
result: *c.PGresult,
next_row_index: c_int = 0,
pub fn rowCount(self: Results) usize {
return @intCast(usize, c.PQntuples(self.result));
}
pub fn row(self: *Results) !?Row {
if (self.next_row_index >= self.rowCount()) return null;
const idx = self.next_row_index;
self.next_row_index += 1;
return Row{
.result = self.result,
.row_index = idx,
};
}
pub fn columnCount(self: Results) u15 {
return @intCast(u15, c.PQnfields(self.result));
}
pub fn columnNameToIndex(self: Results, name: []const u8) !u15 {
const idx = c.PQfnumber(self.result, name.ptr);
if (idx == -1) return error.ColumnNotFound;
return @intCast(u15, idx);
}
pub fn finish(self: Results) void {
c.PQclear(self.result);
}
};
pub const Row = struct {
result: *c.PGresult,
row_index: c_int,
pub fn get(self: Row, comptime T: type, idx: u16, alloc: ?Allocator) !T {
const val = c.PQgetvalue(self.result, self.row_index, idx);
const is_null = (c.PQgetisnull(self.result, self.row_index, idx) != 0);
if (is_null) {
return if (@typeInfo(T) == .Optional) null else error.NullValue;
}
if (val == null) return error.Unexpected;
const len = @intCast(usize, c.PQgetlength(self.result, self.row_index, idx));
return try common.parseValueNotNull(alloc, T, val[0..len]);
}
};
pub const Db = struct {
conn: *c.PGconn,
pub fn open(conn_str: [:0]const u8) !Db {
const conn = c.PQconnectdb(conn_str.ptr) orelse {
std.log.err("Unable to connect to database", .{});
return error.UnknownError;
};
errdefer c.PQfinish(conn);
std.log.info("Connecting to database using provided connection string...", .{});
loop: while (true) {
switch (c.PQstatus(conn)) {
c.CONNECTION_OK => break :loop,
c.CONNECTION_BAD => {
std.log.err("Error connecting to database", .{});
return error.BadConnection;
},
else => std.os.nanosleep(0, 100 * 1000 * 1000), // 100 ms
}
}
std.log.info("DB connection established", .{});
return Db{ .conn = conn };
}
pub fn close(self: Db) void {
c.PQfinish(self.conn);
}
const format_text = 0;
const format_binary = 1;
pub fn exec(self: Db, sql: [:0]const u8, args: anytype, alloc: ?Allocator) !Results {
const result = blk: {
if (comptime args.len > 0) {
var arena = std.heap.ArenaAllocator.init(alloc orelse return error.AllocatorRequired);
defer arena.deinit();
const params = try arena.allocator().alloc(?[*:0]const u8, args.len);
inline for (args) |arg, i| params[i] = if (try common.prepareParamText(&arena, arg)) |slice| slice.ptr else null;
break :blk c.PQexecParams(self.conn, sql.ptr, @intCast(c_int, params.len), null, params.ptr, null, null, format_text);
} else {
break :blk c.PQexecParams(self.conn, sql.ptr, 0, null, null, null, null, format_text);
}
} orelse {
std.log.err("Error occurred in sql query: {?s}", .{c.PQerrorMessage(self.conn)});
return error.UnknownError;
};
errdefer c.PQclear(result);
const status = c.PQresultStatus(result);
return switch (status) {
c.PGRES_EMPTY_QUERY => error.EmptyQuery,
c.PGRES_FATAL_ERROR => blk: {
std.log.err("Error occurred in sql query: {?s}", .{c.PQresultErrorMessage(result)});
break :blk error.UnknownError;
},
c.PGRES_BAD_RESPONSE => blk: {
std.log.err("Error occurred in sql query: {?s}", .{c.PQresultErrorMessage(result)});
break :blk error.BadResponse;
},
c.PGRES_COMMAND_OK, c.PGRES_TUPLES_OK, c.PGRES_COPY_OUT, c.PGRES_COPY_IN, c.PGRES_COPY_BOTH => return Results{ .result = result }, //TODO
c.PGRES_SINGLE_TUPLE => unreachable, // Not yet supported
else => unreachable,
};
}
};