From c6d80fd4d3a3aff28a72f350957d7c58f8137fdd Mon Sep 17 00:00:00 2001 From: jaina heartles Date: Sat, 1 Oct 2022 02:05:33 -0700 Subject: [PATCH] Sql refactor --- src/sql/{ => engines}/common.zig | 56 ++++- src/sql/engines/postgres.zig | 220 +++++++++++++++++ src/sql/engines/postgres/c.zig | 3 + src/sql/engines/postgres/errors.zig | 341 ++++++++++++++++++++++++++ src/sql/{ => engines}/sqlite.zig | 70 +++--- src/sql/errors.zig | 105 ++++++++ src/sql/lib.zig | 365 ++++++++++++++++------------ src/sql/postgres.zig | 134 ---------- 8 files changed, 967 insertions(+), 327 deletions(-) rename src/sql/{ => engines}/common.zig (66%) create mode 100644 src/sql/engines/postgres.zig create mode 100644 src/sql/engines/postgres/c.zig create mode 100644 src/sql/engines/postgres/errors.zig rename src/sql/{ => engines}/sqlite.zig (83%) create mode 100644 src/sql/errors.zig delete mode 100644 src/sql/postgres.zig diff --git a/src/sql/common.zig b/src/sql/engines/common.zig similarity index 66% rename from src/sql/common.zig rename to src/sql/engines/common.zig index 3b2c5e6..a5dbc5b 100644 --- a/src/sql/common.zig +++ b/src/sql/engines/common.zig @@ -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) { diff --git a/src/sql/engines/postgres.zig b/src/sql/engines/postgres.zig new file mode 100644 index 0000000..e54f462 --- /dev/null +++ b/src/sql/engines/postgres.zig @@ -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; + }, + } + } +}; diff --git a/src/sql/engines/postgres/c.zig b/src/sql/engines/postgres/c.zig new file mode 100644 index 0000000..82270f4 --- /dev/null +++ b/src/sql/engines/postgres/c.zig @@ -0,0 +1,3 @@ +usingnamespace @cImport({ + @cInclude("libpq-fe.h"); +}); diff --git a/src/sql/engines/postgres/errors.zig b/src/sql/engines/postgres/errors.zig new file mode 100644 index 0000000..1d91a17 --- /dev/null +++ b/src/sql/engines/postgres/errors.zig @@ -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"), + + _, +}; diff --git a/src/sql/sqlite.zig b/src/sql/engines/sqlite.zig similarity index 83% rename from src/sql/sqlite.zig rename to src/sql/engines/sqlite.zig index 77fc457..7d0a93f 100644 --- a/src/sql/sqlite.zig +++ b/src/sql/engines/sqlite.zig @@ -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]; diff --git a/src/sql/errors.zig b/src/sql/errors.zig new file mode 100644 index 0000000..11db043 --- /dev/null +++ b/src/sql/errors.zig @@ -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; +}; diff --git a/src/sql/lib.zig b/src/sql/lib.zig index 8108c44..cc0bf97 100644 --- a/src/sql/lib.zig +++ b/src/sql/lib.zig @@ -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; } }; diff --git a/src/sql/postgres.zig b/src/sql/postgres.zig deleted file mode 100644 index 5fe217f..0000000 --- a/src/sql/postgres.zig +++ /dev/null @@ -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, - }; - } -};