introduce ddb.zig to serve as core DB operations/move core into ddb.zig
This commit is contained in:
		
							parent
							
								
									e94184419e
								
							
						
					
					
						commit
						796eed8de0
					
				
					 4 changed files with 284 additions and 102 deletions
				
			
		|  | @ -14,4 +14,5 @@ pub fn handler(request: *AuthenticatedRequest, writer: anytype) ![]const u8 { | |||
|     var parsed = try std.json.parseFromSlice(std.json.Value, allocator, request.event_data, .{}); | ||||
|     defer parsed.deinit(); | ||||
|     // const request_params = try parseRequest(request, parsed, writer); | ||||
|     return "hi"; | ||||
| } | ||||
|  |  | |||
|  | @ -505,6 +505,7 @@ pub fn handler(request: *AuthenticatedRequest, writer: anytype) ![]const u8 { | |||
|     //    3. Delete any existing records with that hash value (for delete requests, we're done here) | ||||
|     //    4. If put request, put the new item in the table (with encrypted values, using table encryption) | ||||
|     // TODO: Capacity limiting and metrics | ||||
|     return "hi"; | ||||
| } | ||||
| 
 | ||||
| test "basic request parsing failure" { | ||||
|  |  | |||
|  | @ -4,6 +4,7 @@ const AuthenticatedRequest = @import("AuthenticatedRequest.zig"); | |||
| const Account = @import("Account.zig"); | ||||
| const encryption = @import("encryption.zig"); | ||||
| const returnException = @import("main.zig").returnException; | ||||
| const ddb = @import("ddb.zig"); | ||||
| const ddb_types = @import("ddb_types.zig"); | ||||
| 
 | ||||
| // These are in the original casing so as to make the error messages nice | ||||
|  | @ -15,17 +16,9 @@ const RequiredFields = enum(u3) { | |||
|     // zig fmt: on | ||||
| }; | ||||
| 
 | ||||
| const TableInfo = struct { | ||||
|     attribute_definitions: []*ddb_types.AttributeDefinition, | ||||
|     // gsi_list: []const u8, // Not sure how this is used | ||||
|     // gsi_description_list: []const u8, // Not sure how this is used | ||||
|     // sqlite_index: []const u8, // Not sure how this is used | ||||
|     table_key: [encryption.encoded_key_length]u8, | ||||
| }; | ||||
| 
 | ||||
| const Params = struct { | ||||
|     table_name: []const u8, | ||||
|     table_info: TableInfo, | ||||
|     table_info: ddb_types.TableInfo, | ||||
|     read_capacity_units: ?i64 = null, | ||||
|     write_capacity_units: ?i64 = null, | ||||
|     billing_mode_pay_per_request: bool = false, | ||||
|  | @ -47,23 +40,18 @@ pub fn handler(request: *AuthenticatedRequest, writer: anytype) ![]const u8 { | |||
|     var db = try Account.dbForAccount(allocator, account_id); | ||||
|     const account = try Account.accountForId(allocator, account_id); // This will get us the encryption key needed | ||||
|     defer account.deinit(); | ||||
|     // TODO: better to do all encryption when request params are parsed? | ||||
|     const table_name = try encryption.encryptAndEncode(allocator, account.root_account_key.*, request_params.table_name); | ||||
|     defer allocator.free(table_name); | ||||
|     // We'll json serialize our table_info structure, encrypt, encode, and plow in | ||||
|     const table_info_string = try std.json.stringifyAlloc(allocator, request_params.table_info, .{ .whitespace = .indent_2 }); | ||||
|     defer allocator.free(table_info_string); | ||||
|     const table_info = try encryption.encryptAndEncode(allocator, account.root_account_key.*, table_info_string); | ||||
|     defer allocator.free(table_info); | ||||
| 
 | ||||
|     try insertIntoDm( | ||||
|     try ddb.createDdbTable( | ||||
|         allocator, | ||||
|         &db, | ||||
|         table_name, | ||||
|         table_info, | ||||
|         account, | ||||
|         request_params.table_name, | ||||
|         request_params.table_info, | ||||
|         request_params.read_capacity_units orelse 0, | ||||
|         request_params.write_capacity_units orelse 0, | ||||
|         request_params.billing_mode_pay_per_request, | ||||
|     ); | ||||
| 
 | ||||
|     // Server side Input validation error on live DDB results in this for a 2 char table name | ||||
|     // 400 - bad request | ||||
|     // {"__type":"com.amazon.coral.validate#ValidationException","message":"TableName must be at least 3 characters long and at most 255 characters long"} | ||||
|  | @ -127,39 +115,6 @@ pub fn handler(request: *AuthenticatedRequest, writer: anytype) ![]const u8 { | |||
|     //   "UniqueGSIIndexes": [] | ||||
|     // } | ||||
|     // | ||||
|     var diags = sqlite.Diagnostics{}; | ||||
| 
 | ||||
|     // It doesn't seem that I can bind a variable here. But it actually doesn't matter as we're | ||||
|     // encoding the name... | ||||
|     // IF NOT EXISTS doesn't apply - we want this to bounce if the table exists | ||||
|     const create_stmt = try std.fmt.allocPrint(allocator, | ||||
|         \\CREATE TABLE '{s}' ( | ||||
|         \\    hashKey TEXT DEFAULT NULL, | ||||
|         \\    rangeKey TEXT DEFAULT NULL, | ||||
|         \\    hashValue BLOB NOT NULL, | ||||
|         \\    rangeValue BLOB NOT NULL, | ||||
|         \\    itemSize INTEGER DEFAULT 0, | ||||
|         \\    ObjectJSON BLOB NOT NULL, | ||||
|         \\    PRIMARY KEY(hashKey, rangeKey) | ||||
|         \\) | ||||
|     , .{table_name}); | ||||
|     defer allocator.free(create_stmt); | ||||
|     // db.exec requires a comptime statement. execDynamic does not | ||||
|     db.execDynamic( | ||||
|         create_stmt, | ||||
|         .{ .diags = &diags }, | ||||
|         .{}, | ||||
|     ) catch |e| { | ||||
|         std.log.debug("SqlLite Diags: {}", .{diags}); | ||||
|         return e; | ||||
|     }; | ||||
|     const create_index_stmt = try std.fmt.allocPrint( | ||||
|         allocator, | ||||
|         "CREATE INDEX \"{s}*HVI\" ON \"{s}\" (hashValue)", | ||||
|         .{ table_name, table_name }, | ||||
|     ); | ||||
|     defer allocator.free(create_index_stmt); | ||||
|     try db.execDynamic(create_index_stmt, .{}, .{}); | ||||
| 
 | ||||
|     var al = std.ArrayList(u8).init(allocator); | ||||
|     var response_writer = al.writer(); | ||||
|  | @ -167,54 +122,6 @@ pub fn handler(request: *AuthenticatedRequest, writer: anytype) ![]const u8 { | |||
|     return al.toOwnedSlice(); | ||||
| } | ||||
| 
 | ||||
| fn insertIntoDm( | ||||
|     db: *sqlite.Db, | ||||
|     table_name: []const u8, | ||||
|     table_info: []const u8, | ||||
|     read_capacity_units: i64, | ||||
|     write_capacity_units: i64, | ||||
|     billing_mode_pay_per_request: bool, | ||||
| ) !void { | ||||
|     // const current_time = std.time.nanotimestamp(); | ||||
|     const current_time = std.time.microTimestamp(); // SQLlite integers are only 64bit max | ||||
|     try db.exec( | ||||
|         \\INSERT INTO dm( | ||||
|         \\  TableName, | ||||
|         \\  CreationDateTime, | ||||
|         \\  LastDecreaseDate, | ||||
|         \\  LastIncreaseDate, | ||||
|         \\  NumberOfDecreasesToday, | ||||
|         \\  ReadCapacityUnits, | ||||
|         \\  WriteCapacityUnits, | ||||
|         \\  TableInfo, | ||||
|         \\  BillingMode, | ||||
|         \\  PayPerRequestDateTime | ||||
|         \\  ) VALUES ( | ||||
|         \\  $tablename{[]const u8}, | ||||
|         \\  $createdate{i64}, | ||||
|         \\  $lastdecreasedate{usize}, | ||||
|         \\  $lastincreasedate{usize}, | ||||
|         \\  $numberofdecreasestoday{usize}, | ||||
|         \\  $readcapacityunits{i64}, | ||||
|         \\  $writecapacityunits{i64}, | ||||
|         \\  $tableinfo{[]const u8}, | ||||
|         \\  $billingmode{usize}, | ||||
|         \\  $payperrequestdatetime{usize} | ||||
|         \\  ) | ||||
|     , .{}, .{ | ||||
|         table_name, | ||||
|         current_time, | ||||
|         @as(usize, 0), | ||||
|         @as(usize, 0), | ||||
|         @as(usize, 0), | ||||
|         read_capacity_units, | ||||
|         write_capacity_units, | ||||
|         table_info, | ||||
|         if (billing_mode_pay_per_request) @as(usize, 1) else @as(usize, 0), | ||||
|         @as(usize, 0), | ||||
|     }); | ||||
| } | ||||
| 
 | ||||
| fn parseRequest( | ||||
|     request: *AuthenticatedRequest, | ||||
|     parsed: std.json.Parsed(std.json.Value), | ||||
|  | @ -273,7 +180,7 @@ fn parseRequest( | |||
|     errdefer { | ||||
|         if (attribute_definitions_assigned) { | ||||
|             for (request_params.table_info.attribute_definitions) |d| { | ||||
|                 request.allocator.free(d.*.name); | ||||
|                 request.allocator.free(d.name); | ||||
|                 request.allocator.destroy(d); | ||||
|             } | ||||
|             request.allocator.free(request_params.table_info.attribute_definitions); | ||||
|  |  | |||
							
								
								
									
										273
									
								
								src/ddb.zig
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										273
									
								
								src/ddb.zig
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,273 @@ | |||
| const std = @import("std"); | ||||
| const sqlite = @import("sqlite"); | ||||
| const AuthenticatedRequest = @import("AuthenticatedRequest.zig"); | ||||
| const Account = @import("Account.zig"); | ||||
| const ddb_types = @import("ddb_types.zig"); | ||||
| const encryption = @import("encryption.zig"); | ||||
| const builtin = @import("builtin"); | ||||
| 
 | ||||
| pub const TableArray = struct { | ||||
|     items: []Table, | ||||
|     allocator: std.mem.Allocator, | ||||
| 
 | ||||
|     pub fn init(allocator: std.mem.Allocator, length: usize) !TableArray { | ||||
|         return .{ | ||||
|             .allocator = allocator, | ||||
|             .items = try allocator.alloc(Table, length), | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     pub fn deinit(self: *TableArray) void { | ||||
|         for (self.items) |*item| | ||||
|             item.deinit(); | ||||
|         self.allocator.free(self.items); | ||||
|     } | ||||
| }; | ||||
| pub const Table = struct { | ||||
|     table_name: []const u8, | ||||
|     table_key: [encryption.key_length]u8, | ||||
|     allocator: std.mem.Allocator, | ||||
| 
 | ||||
|     pub fn deinit(self: *Table) void { | ||||
|         std.crypto.utils.secureZero(u8, &self.table_key); | ||||
|         self.allocator.free(self.table_name); | ||||
|     } | ||||
| }; | ||||
| 
 | ||||
| // Gets all table names/keys for the account. Caller owns returned array | ||||
| pub fn tablesForAccount(allocator: std.mem.Allocator, account_id: []const u8) !TableArray { | ||||
|     var db = try Account.dbForAccount(allocator, account_id); | ||||
|     defer if (!builtin.is_test) db.deinit(); | ||||
|     const account = try Account.accountForId(allocator, account_id); // This will get us the encryption key needed | ||||
|     defer account.deinit(); | ||||
| 
 | ||||
|     const query = | ||||
|         \\SELECT TableName as table_name, TableInfo as table_info FROM dm | ||||
|     ; | ||||
| 
 | ||||
|     var stmt = try db.prepare(query); | ||||
|     defer stmt.deinit(); | ||||
| 
 | ||||
|     const rows = try stmt.all(struct { | ||||
|         table_name: []const u8, | ||||
|         table_info: []const u8, | ||||
|     }, allocator, .{}, .{}); | ||||
|     defer allocator.free(rows); | ||||
|     var rc = try TableArray.init(allocator, rows.len); | ||||
|     errdefer rc.deinit(); | ||||
| 
 | ||||
|     // std.debug.print(" \n===\nRow count: {d}\n===\n", .{rows.len}); | ||||
|     for (rows, 0..) |row, inx| { | ||||
|         defer allocator.free(row.table_name); | ||||
|         defer allocator.free(row.table_info); | ||||
|         const table_name = try encryption.decodeAndDecrypt( | ||||
|             allocator, | ||||
|             account.root_account_key.*, | ||||
|             row.table_name, | ||||
|         ); | ||||
|         errdefer allocator.free(table_name); | ||||
|         const table_info_str = try encryption.decodeAndDecrypt( | ||||
|             allocator, | ||||
|             account.root_account_key.*, | ||||
|             row.table_info, | ||||
|         ); | ||||
|         defer allocator.free(table_info_str); | ||||
|         // std.debug.print(" \n===TableInfo: {s}\n===\n", .{table_info_str}); | ||||
|         const table_info = try std.json.parseFromSlice(ddb_types.TableInfo, allocator, table_info_str, .{}); | ||||
|         defer table_info.deinit(); | ||||
|         // errdefer allocator.free(table_info.table_key); | ||||
|         // defer { | ||||
|         //     // we don't even really need to defer this... | ||||
|         //     for (table_info.value.attribute_definitions) |*def| { | ||||
|         //         allocator.free(def.*.name); | ||||
|         //         allocator.destroy(def); | ||||
|         //     } | ||||
|         //     allocator.free(table_info.table_key); | ||||
|         // } | ||||
| 
 | ||||
|         rc.items[inx] = .{ | ||||
|             .allocator = allocator, | ||||
|             .table_name = table_name, | ||||
|             .table_key = undefined, | ||||
|         }; | ||||
|         try encryption.decodeKey(&rc.items[inx].table_key, table_info.value.table_key); | ||||
|     } | ||||
|     return rc; | ||||
| } | ||||
| 
 | ||||
| /// creates a table in the underlying storage | ||||
| pub fn createDdbTable( | ||||
|     allocator: std.mem.Allocator, | ||||
|     db: *sqlite.Db, | ||||
|     account: Account, | ||||
|     table_name: []const u8, | ||||
|     table_info: ddb_types.TableInfo, | ||||
|     read_capacity_units: i64, | ||||
|     write_capacity_units: i64, | ||||
|     billing_mode_pay_per_request: bool, | ||||
| ) !void { | ||||
|     const encrypted_table_name = try insertIntoDatabaseMetadata( | ||||
|         allocator, | ||||
|         db, | ||||
|         account, | ||||
|         table_name, | ||||
|         table_info, | ||||
|         read_capacity_units, | ||||
|         write_capacity_units, | ||||
|         billing_mode_pay_per_request, | ||||
|     ); | ||||
|     defer allocator.free(encrypted_table_name); | ||||
| 
 | ||||
|     var diags = sqlite.Diagnostics{}; | ||||
| 
 | ||||
|     // It doesn't seem that I can bind a variable here. But it actually doesn't matter as we're | ||||
|     // encoding the name... | ||||
|     // IF NOT EXISTS doesn't apply - we want this to bounce if the table exists | ||||
|     const create_stmt = try std.fmt.allocPrint(allocator, | ||||
|         \\CREATE TABLE '{s}' ( | ||||
|         \\    hashKey TEXT DEFAULT NULL, | ||||
|         \\    rangeKey TEXT DEFAULT NULL, | ||||
|         \\    hashValue BLOB NOT NULL, | ||||
|         \\    rangeValue BLOB NOT NULL, | ||||
|         \\    itemSize INTEGER DEFAULT 0, | ||||
|         \\    ObjectJSON BLOB NOT NULL, | ||||
|         \\    PRIMARY KEY(hashKey, rangeKey) | ||||
|         \\) | ||||
|     , .{encrypted_table_name}); | ||||
|     defer allocator.free(create_stmt); | ||||
|     // db.exec requires a comptime statement. execDynamic does not | ||||
|     db.execDynamic( | ||||
|         create_stmt, | ||||
|         .{ .diags = &diags }, | ||||
|         .{}, | ||||
|     ) catch |e| { | ||||
|         std.log.debug("SqlLite Diags: {}", .{diags}); | ||||
|         return e; | ||||
|     }; | ||||
|     const create_index_stmt = try std.fmt.allocPrint( | ||||
|         allocator, | ||||
|         "CREATE INDEX \"{s}*HVI\" ON \"{s}\" (hashValue)", | ||||
|         .{ encrypted_table_name, encrypted_table_name }, | ||||
|     ); | ||||
|     defer allocator.free(create_index_stmt); | ||||
|     try db.execDynamic(create_index_stmt, .{}, .{}); | ||||
| } | ||||
| 
 | ||||
| /// Inserts a new table into the database metadata (dm) table. Handles encryption | ||||
| /// Returns encrypted table name | ||||
| fn insertIntoDatabaseMetadata( | ||||
|     allocator: std.mem.Allocator, | ||||
|     db: *sqlite.Db, | ||||
|     account: Account, | ||||
|     table_name: []const u8, | ||||
|     table_info: ddb_types.TableInfo, | ||||
|     read_capacity_units: i64, | ||||
|     write_capacity_units: i64, | ||||
|     billing_mode_pay_per_request: bool, | ||||
| ) ![]const u8 { | ||||
|     // TODO: better to do all encryption when request params are parsed? | ||||
|     const encrypted_table_name = try encryption.encryptAndEncode(allocator, account.root_account_key.*, table_name); | ||||
|     errdefer allocator.free(encrypted_table_name); | ||||
|     // We'll json serialize our table_info structure, encrypt, encode, and plow in | ||||
|     const table_info_string = try std.json.stringifyAlloc(allocator, table_info, .{ .whitespace = .indent_2 }); | ||||
|     defer allocator.free(table_info_string); | ||||
|     const encrypted_table_info = try encryption.encryptAndEncode(allocator, account.root_account_key.*, table_info_string); | ||||
|     defer allocator.free(encrypted_table_info); | ||||
|     try insertIntoDm(db, encrypted_table_name, encrypted_table_info, read_capacity_units, write_capacity_units, billing_mode_pay_per_request); | ||||
|     return encrypted_table_name; | ||||
| } | ||||
| 
 | ||||
| fn insertIntoDm( | ||||
|     db: *sqlite.Db, | ||||
|     table_name: []const u8, | ||||
|     table_info: []const u8, | ||||
|     read_capacity_units: i64, | ||||
|     write_capacity_units: i64, | ||||
|     billing_mode_pay_per_request: bool, | ||||
| ) !void { | ||||
|     // const current_time = std.time.nanotimestamp(); | ||||
|     const current_time = std.time.microTimestamp(); // SQLlite integers are only 64bit max | ||||
|     try db.exec( | ||||
|         \\INSERT INTO dm( | ||||
|         \\  TableName, | ||||
|         \\  CreationDateTime, | ||||
|         \\  LastDecreaseDate, | ||||
|         \\  LastIncreaseDate, | ||||
|         \\  NumberOfDecreasesToday, | ||||
|         \\  ReadCapacityUnits, | ||||
|         \\  WriteCapacityUnits, | ||||
|         \\  TableInfo, | ||||
|         \\  BillingMode, | ||||
|         \\  PayPerRequestDateTime | ||||
|         \\  ) VALUES ( | ||||
|         \\  $tablename{[]const u8}, | ||||
|         \\  $createdate{i64}, | ||||
|         \\  $lastdecreasedate{usize}, | ||||
|         \\  $lastincreasedate{usize}, | ||||
|         \\  $numberofdecreasestoday{usize}, | ||||
|         \\  $readcapacityunits{i64}, | ||||
|         \\  $writecapacityunits{i64}, | ||||
|         \\  $tableinfo{[]const u8}, | ||||
|         \\  $billingmode{usize}, | ||||
|         \\  $payperrequestdatetime{usize} | ||||
|         \\  ) | ||||
|     , .{}, .{ | ||||
|         table_name, | ||||
|         current_time, | ||||
|         @as(usize, 0), | ||||
|         @as(usize, 0), | ||||
|         @as(usize, 0), | ||||
|         read_capacity_units, | ||||
|         write_capacity_units, | ||||
|         table_info, | ||||
|         if (billing_mode_pay_per_request) @as(usize, 1) else @as(usize, 0), | ||||
|         @as(usize, 0), | ||||
|     }); | ||||
| } | ||||
| 
 | ||||
| fn testCreateTable(allocator: std.mem.Allocator, account_id: []const u8) !sqlite.Db { | ||||
|     var db = try Account.dbForAccount(allocator, account_id); | ||||
|     const account = try Account.accountForId(allocator, account_id); // This will get us the encryption key needed | ||||
|     defer account.deinit(); | ||||
|     var hash = ddb_types.AttributeDefinition{ .name = "Artist", .type = .S }; | ||||
|     var range = ddb_types.AttributeDefinition{ .name = "SongTitle", .type = .S }; | ||||
|     var definitions = @constCast(&[_]*ddb_types.AttributeDefinition{ | ||||
|         &hash, | ||||
|         &range, | ||||
|     }); | ||||
|     var table_info: ddb_types.TableInfo = .{ | ||||
|         .table_key = undefined, | ||||
|         .attribute_definitions = definitions[0..], | ||||
|     }; | ||||
|     encryption.randomEncodedKey(&table_info.table_key); | ||||
|     try createDdbTable( | ||||
|         allocator, | ||||
|         &db, | ||||
|         account, | ||||
|         "MusicCollection", | ||||
|         table_info, | ||||
|         5, | ||||
|         5, | ||||
|         false, | ||||
|     ); | ||||
|     return db; | ||||
| } | ||||
| test "can create a table" { | ||||
|     const allocator = std.testing.allocator; | ||||
|     const account_id = "1234"; | ||||
|     var db = try testCreateTable(allocator, account_id); | ||||
|     defer db.deinit(); | ||||
| } | ||||
| test "can list tables in an account" { | ||||
|     Account.test_retain_db = true; | ||||
|     defer Account.test_retain_db = false; | ||||
|     const allocator = std.testing.allocator; | ||||
|     const account_id = "1234"; | ||||
|     var db = try testCreateTable(allocator, account_id); | ||||
|     defer db.deinit(); | ||||
|     var table_list = try tablesForAccount(allocator, account_id); | ||||
|     defer table_list.deinit(); | ||||
|     try std.testing.expectEqual(@as(usize, 1), table_list.items.len); | ||||
|     try std.testing.expectEqualStrings("MusicCollection", table_list.items[0].table_name); | ||||
|     // std.debug.print(" \n===\nKey: {s}\n===\n", .{std.fmt.fmtSliceHexLower(&table_list.items[0].table_key)}); | ||||
| } | ||||
		Loading…
	
	Add table
		
		Reference in a new issue