├── .gitattributes ├── .github └── workflows │ ├── build-platforms.yml │ └── test.yml ├── .gitignore ├── .gitmodules ├── LICENSE ├── README.md ├── build.zig ├── build.zig.zon ├── examples ├── 01_basic_connect.zig ├── 02_connect_and_create_db.zig ├── 03_create_and_query_table.zig └── 04_row_binding.zig └── src ├── Connection.zig ├── Cursor.zig ├── ParameterBucket.zig ├── ResultSet.zig ├── catalog.zig ├── util.zig └── zdb.zig /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=false 2 | 3 | *.zig text eol=lf -------------------------------------------------------------------------------- /.github/workflows/build-platforms.yml: -------------------------------------------------------------------------------- 1 | name: build-platforms 2 | 3 | # Controls when the action will run. 4 | on: 5 | # Triggers the workflow on push or pull request events but only for the main branch 6 | push: 7 | branches: [ main ] 8 | pull_request: 9 | branches: [ main ] 10 | 11 | # Allows you to run this workflow manually from the Actions tab 12 | workflow_dispatch: 13 | 14 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 15 | jobs: 16 | # This workflow contains a single job called "build" 17 | test-build: 18 | # The type of runner that the job will run on 19 | runs-on: ${{ matrix.os }} 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | os: [ubuntu-latest, windows-latest, macos-latest] 24 | 25 | # Steps represent a sequence of tasks that will be executed as part of the job 26 | steps: 27 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 28 | - uses: actions/checkout@v2 29 | with: 30 | submodules: recursive 31 | 32 | - uses: goto-bus-stop/setup-zig@v1 33 | with: 34 | version: master 35 | 36 | - name: Build Project on ${{ matrix.os }} 37 | run: zig build 38 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: run-tests 2 | 3 | # Controls when the action will run. 4 | on: push 5 | 6 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 7 | jobs: 8 | # This workflow contains a single job called "build" 9 | run-test: 10 | # The type of runner that the job will run on 11 | runs-on: ubuntu-latest 12 | 13 | # Steps represent a sequence of tasks that will be executed as part of the job 14 | steps: 15 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 16 | - uses: actions/checkout@v2 17 | with: 18 | submodules: recursive 19 | 20 | - uses: goto-bus-stop/setup-zig@v1 21 | with: 22 | version: master 23 | 24 | - name: Run tests 25 | run: zig build test 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | zig-cache 2 | zig-out 3 | .vscode -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjoerussell/zdb/955ef2e57ac8a2b6e89b6e8c1c65b2f2bcb0fec1/.gitmodules -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Mitchell Russell 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # zdb 2 | 3 | A library for interacting with databases in Zig. Builds on top of [zig-odbc](https://github.com/mjoerussell/zig-odbc) to provide a higher-level 4 | interaction between the developer and the DB. 5 | 6 | **Important!: This project is still not fully production-ready. The biggest missing piece as of now is that ODBC operations can only run synchronously.** 7 | 8 | ## Using this Library 9 | 10 | To use zdb, follow these steps: 11 | 12 | ### 0. Dependencies 13 | 14 | Make sure you have an ODBC driver and driver manager installed already. On Windows, this should be pre-installed. On other 15 | systems, a good option is [`unixODBC`](http://www.unixodbc.org). 16 | 17 | ### 1. Add to `build.zig.zon` 18 | 19 | ```zig 20 | .{ 21 | .name = "", 22 | .version = "", 23 | .dependencies = .{ 24 | .zdb = .{ 25 | .url = "https://github.com/mjoerussell/zdb/.tar.gz", 26 | .hash = "", 27 | } 28 | } 29 | } 30 | ``` 31 | 32 | ### 2. Add zdb module & artifact to your project 33 | 34 | ```zig 35 | 36 | pub fn build(b: *std.build.Builder) void { 37 | // Create executable "exe" 38 | 39 | var zdb_dep = b.dependency("zdb", .{ 40 | .target = target, 41 | .optimize = optimize, 42 | }); 43 | 44 | const zdb_module = zdb_dep.module("zdb"); 45 | const zdb_lib = zdb_dep.artifact("zdb"); 46 | 47 | exe.addModule("zdb", zdb_module); 48 | exe.linkLibrary(zdb_lib); 49 | } 50 | ``` 51 | 52 | ### 3. Usage in Code 53 | 54 | Wherever you use zdb, include `const zdb = @import("zdb");`. 55 | 56 | ## Current Features 57 | 58 | Currently this library is in alpha and is limited in scope. The currently available features include: 59 | 60 | ### Connect to Database 61 | 62 | It's easy to connect to a database using a connection string: 63 | 64 | ```zig 65 | const zdb = @import("zdb"); 66 | const Connection = zdb.Connection; 67 | 68 | pub fn main() !void { 69 | var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 70 | defer _ = gpa.deinit(); 71 | 72 | const allocator = &gpa.allocator; 73 | 74 | var connection = try Connection.init(.{}); 75 | defer connection.deinit(); 76 | 77 | try connection.connectExtended("ODBC;driver=PostgreSQL Unicode(x64);DSN=PostgreSQL35W"); 78 | } 79 | ``` 80 | 81 | You can also use a configuration struct to connect: 82 | 83 | ```zig 84 | try connection.connectWithConfig(allocator, .{ .driver = "PostgeSQL Unicode(x64)", .dsn = "PostgeSQL35W" }); 85 | ``` 86 | 87 | ### Execute Statements 88 | 89 | Arbitrary SQL statements can be executed with `Cursor.executeDirect`. Prepared statements can also be created and then executed with `Cursor.prepare` and `Cursor.execute`, respectively. 90 | 91 | ```zig 92 | // An example of executing a statement directly 93 | 94 | //..... 95 | 96 | var cursor = try connection.getCursor(allocator); 97 | defer cursor.deinit(); 98 | 99 | var result_set = try cursor.executeDirect(allocator, "select * from example_table", .{}); 100 | 101 | // use results....... 102 | ``` 103 | 104 | Both direct executions and prepared executions support statement parameters. `Cursor.executeDirect` and `Cursor.prepare` support passing parameters as a tuple. Prepared statements can be used with multiple sets of parameters by calling `Cursor.bindParameters` in-between executions. 105 | 106 | ```zig 107 | // An example of executing a query with parameters 108 | 109 | var cursor = try connection.getCursor(allocator); 110 | defer cursor.deinit(allocator); 111 | 112 | var result_set = try cursor.executeDirect(allocator, "select * from example_table where value > ?", .{10}); 113 | 114 | // use results..... 115 | ``` 116 | 117 | ### Insert Data 118 | 119 | You can use `Cursor.executeDirect` and the prepared statement alternative to execute **INSERT** statements just as described in the previous section; however, one often wants to insert multiple values at once. It's also very common to model tables as structs in code, and to want to push entire structs to a table. Because of this zdb has a convenient way to run **INSERT** queries. 120 | 121 | For a complete example of how this feature can be used please refer to the example [03_create_and_query_table](./examples/src/03_create_and_query_table.zig). 122 | 123 | ### ODBC Fallthrough 124 | 125 | If you want to use this package in it's current state, then it would probably be necessary to use the ODBC bindings directly to 126 | supplement missing features. You can access the bindings by importing them like this: 127 | 128 | ``` 129 | const odbc = @import("zdb").odbc; 130 | ``` 131 | 132 | Please see [zig-odbc](https://github.com/mjoerussell/zig-odbc) for more information about these bindings. 133 | -------------------------------------------------------------------------------- /build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const builtin = std.builtin; 3 | const CompileStep = std.build.CompileStep; 4 | 5 | const test_files = .{ 6 | "src/ParameterBucket.zig", 7 | "src/Connection.zig", 8 | }; 9 | 10 | const Example = struct { 11 | name: []const u8, 12 | source_file: std.Build.FileSource, 13 | description: []const u8, 14 | }; 15 | 16 | const examples = [_]Example{ 17 | .{ 18 | .name = "basic-connect", 19 | .source_file = .{ .path = "examples/01_basic_connect.zig" }, 20 | .description = "Beginner example - configure a connection string and connect to a DB", 21 | }, 22 | .{ 23 | .name = "connect-create", 24 | .source_file = .{ .path = "examples/02_connect_and_create_db.zig" }, 25 | .description = "Connect to a DB, create a new DB, and then reconnect to that new DB", 26 | }, 27 | .{ 28 | .name = "create-table", 29 | .source_file = .{ .path = "examples/03_create_and_query_table.zig" }, 30 | .description = "Create a new table, insert data into the table, and query data from the table", 31 | }, 32 | .{ 33 | .name = "row-binding", 34 | .source_file = .{ .path = "examples/04_row_binding.zig" }, 35 | .description = "Query data and extract results using a RowIterator", 36 | }, 37 | }; 38 | 39 | pub fn build(b: *std.build.Builder) !void { 40 | const optimize = b.standardOptimizeOption(.{}); 41 | const target = b.standardTargetOptions(.{}); 42 | 43 | const zig_odbc_dep = b.dependency("zig_odbc", .{ 44 | .target = target, 45 | .optimize = optimize, 46 | }); 47 | 48 | var zdb_module = b.createModule(.{ 49 | .source_file = .{ .path = "src/zdb.zig" }, 50 | .dependencies = &.{ 51 | .{ .name = "odbc", .module = zig_odbc_dep.module("zig-odbc") }, 52 | }, 53 | }); 54 | 55 | var lib = b.addStaticLibrary(.{ 56 | .name = "zdb", 57 | .optimize = optimize, 58 | .target = target, 59 | }); 60 | 61 | const odbc_lib = zig_odbc_dep.artifact("odbc"); 62 | lib.linkLibrary(odbc_lib); 63 | 64 | inline for (examples) |example| { 65 | const example_exe = b.addExecutable(.{ 66 | .name = example.name, 67 | .root_source_file = example.source_file, 68 | .optimize = optimize, 69 | .target = target, 70 | }); 71 | 72 | example_exe.addModule("zdb", zdb_module); 73 | example_exe.linkLibrary(odbc_lib); 74 | 75 | const install_step = b.addInstallArtifact(example_exe, .{}); 76 | 77 | const run_cmd = b.addRunArtifact(example_exe); 78 | run_cmd.step.dependOn(&install_step.step); 79 | const run_step = b.step(example.name, example.description); 80 | run_step.dependOn(&run_cmd.step); 81 | } 82 | 83 | const test_step = b.step("test", "Run library tests"); 84 | inline for (test_files) |filename| { 85 | const tests = b.addTest(.{ 86 | .root_source_file = .{ .path = filename }, 87 | .optimize = optimize, 88 | .target = target, 89 | }); 90 | 91 | tests.linkLibrary(odbc_lib); 92 | test_step.dependOn(&tests.step); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /build.zig.zon: -------------------------------------------------------------------------------- 1 | .{ 2 | .name = "zdb", 3 | .version = "0.2.0", 4 | 5 | .dependencies = .{ 6 | .zig_odbc = .{ 7 | .url = "https://github.com/mjoerussell/zig-odbc/archive/main.tar.gz", 8 | .hash = "12201a961f7b538543444f6ca418b0ea5482c18119b4e39cf6a8abfd90471e0497b3", 9 | }, 10 | }, 11 | } 12 | -------------------------------------------------------------------------------- /examples/01_basic_connect.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const zdb = @import("zdb"); 3 | 4 | const Connection = zdb.Connection; 5 | 6 | pub fn main() anyerror!void { 7 | const allocator = std.heap.page_allocator; 8 | 9 | // The first step to using zdb is creating your data source. In this example we'll use the default postgres 10 | // settings and connect without using a DSN. 11 | var connection_info = Connection.ConnectionConfig{ 12 | .driver = "PostgreSQL Unicode(x64)", 13 | .database = "postgres", 14 | .server = "localhost", 15 | .port = "5433", 16 | .username = "postgres", 17 | .password = "postgres", 18 | }; 19 | 20 | // Before connecting, initialize a connection struct with the default settings. 21 | var conn = try Connection.init(.{}); 22 | defer conn.deinit(); 23 | 24 | // connectWithConfig is used to connect to a data source using a connection string, which is generated based on the ConnectionConfig. 25 | // You can also connect to a data source using a DSN name, username, and password with connection.connect() 26 | try conn.connectWithConfig(allocator, connection_info); 27 | 28 | // In order to execute statements, you have to create a Cursor object. 29 | var cursor = try conn.getCursor(allocator); 30 | defer cursor.deinit(allocator) catch {}; 31 | 32 | // We'll run a simple operation on this DB to start - simply querying all the database names assocaiated with this 33 | // connection. Since we connected to a specific DB above, this should only return "postgres" 34 | var catalogs = try cursor.catalogs(allocator); 35 | defer allocator.free(catalogs); 36 | 37 | std.debug.print("Got {} catalogs\n", .{catalogs.len}); 38 | 39 | for (catalogs) |cat| { 40 | std.log.debug("{s}", .{cat}); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /examples/02_connect_and_create_db.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const zdb = @import("zdb"); 3 | 4 | const Connection = zdb.Connection; 5 | 6 | pub fn main() anyerror!void { 7 | const allocator = std.heap.page_allocator; 8 | 9 | // In this example we'll create a new database and reconnect to it. 10 | // The beginning is the same as basic_connect 11 | var basic_connect_config = Connection.ConnectionConfig{ 12 | .driver = "PostgreSQL Unicode(x64)", 13 | .database = "postgres", 14 | .server = "localhost", 15 | .port = "5433", 16 | .username = "postgres", 17 | .password = "postgres", 18 | }; 19 | 20 | var conn = try Connection.init(.{}); 21 | defer conn.deinit(); 22 | 23 | { 24 | try conn.connectWithConfig(allocator, basic_connect_config); 25 | defer conn.disconnect(); 26 | 27 | var cursor = try conn.getCursor(allocator); 28 | defer cursor.deinit(allocator) catch {}; 29 | 30 | // Once you have a cursor you can execute arbitrary SQL statements with executeDirect. executeDirect is a good option 31 | // if you're only planning on executing a statement once. Parameters can be set with the final arg, but in this case 32 | // none are required. 33 | // Query results can be fetched using the ResultSet value returned by executeDirect. We don't care about the result set 34 | // of this query, so we'll ignore it. 35 | _ = try cursor.executeDirect(allocator, "CREATE DATABASE create_example WITH OWNER = postgres", .{}); 36 | } 37 | 38 | // Now that the new DB was created we can connect to it. We'll use the same options as the original connection, 39 | // except with the database field set to "create_example". 40 | var db_connect_config = basic_connect_config; 41 | db_connect_config.database = "create_example"; 42 | 43 | { 44 | // For now, we'll just connect and disconnect without doing anything. 45 | try conn.connectWithConfig(allocator, db_connect_config); 46 | conn.disconnect(); 47 | } 48 | 49 | { 50 | // Now we'll clean up the temp table 51 | try conn.connectWithConfig(allocator, basic_connect_config); 52 | defer conn.disconnect(); 53 | 54 | var cursor = try conn.getCursor(allocator); 55 | defer cursor.deinit(allocator) catch {}; 56 | 57 | _ = try cursor.executeDirect(allocator, "DROP DATABASE create_example", .{}); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /examples/03_create_and_query_table.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const zdb = @import("zdb"); 3 | 4 | const Connection = zdb.Connection; 5 | 6 | pub fn main() anyerror!void { 7 | const allocator = std.heap.page_allocator; 8 | 9 | // In this example we'll create a new DB just like create_and_connect, but we'll also perform some inserts and 10 | // queries on a new table. 11 | // Start by connecting and initializing just like before 12 | var basic_connect_config = Connection.ConnectionConfig{ 13 | .driver = "PostgreSQL Unicode(x64)", 14 | .database = "postgres", 15 | .server = "localhost", 16 | .port = "5433", 17 | .username = "postgres", 18 | .password = "postgres", 19 | }; 20 | 21 | const test_connect_string = try basic_connect_config.getConnectionString(allocator); 22 | defer allocator.free(test_connect_string); 23 | 24 | std.debug.print("Conn String: {s}\n", .{test_connect_string}); 25 | 26 | var conn = try Connection.init(.{}); 27 | defer conn.deinit(); 28 | 29 | { 30 | try conn.connectWithConfig(allocator, basic_connect_config); 31 | defer conn.disconnect(); 32 | 33 | var cursor = try conn.getCursor(allocator); 34 | defer cursor.deinit(allocator) catch {}; 35 | 36 | _ = cursor.executeDirect(allocator, "CREATE DATABASE create_example WITH OWNER = postgres", .{}) catch |err| { 37 | const recs = cursor.getErrors(allocator); 38 | defer allocator.free(recs); 39 | for (recs) |rec| { 40 | std.debug.print("{s}\n", .{rec.error_message}); 41 | } 42 | return err; 43 | }; 44 | } 45 | 46 | var example_connect_config = basic_connect_config; 47 | example_connect_config.database = "create_example"; 48 | 49 | { 50 | // Connect to the DB create_example 51 | try conn.connectWithConfig(allocator, example_connect_config); 52 | defer conn.disconnect(); 53 | 54 | var cursor = try conn.getCursor(allocator); 55 | defer cursor.deinit(allocator) catch {}; 56 | 57 | // We'll create a simple table called zdb_test with standard SQL. Just like the other queries that we've seen so far, 58 | // we're going to throw away the result set because we don't care about it here. 59 | _ = try cursor.executeDirect(allocator, 60 | \\CREATE TABLE zdb_test 61 | \\( 62 | \\ id serial primary key, 63 | \\ first_name text, 64 | \\ age integer default 0 65 | \\) 66 | , .{}); 67 | 68 | // There are several ways to insert data into a table using zdb 69 | // If you want you can stick to executeDirect (although prepare + execute would probably make more sense in this case) and 70 | // execute the statements just like before - there's nothing inherently different about running an insert query vs. a select query 71 | // from zdb's perspective. 72 | // 73 | // Cursor provides a convenience function, "insert", that makes it a bit easier to insert several values at once. "insert" takes care 74 | // of binding parameters from a list of inserted items so that you don't have to handle it manually. However, "insert" does *not* write 75 | // queries for you - this isn't an ORM library. 76 | // 77 | // In this first example, we'll use a struct to set parameters. The number of fields on the struct must match the number of parameter slots 78 | // on the query, otherwise you'll get an error. **The names of the struct fields do not have to match the column names of the table**. 79 | const ZdbTest = struct { 80 | first_name: []const u8, 81 | age: u32, 82 | }; 83 | 84 | // Pass the insert query to run and pass an array of ZdbTest structs containing the params to bind. 85 | // "insert" will create a prepared statement and execute the query with your params. 86 | _ = try cursor.insert( 87 | allocator, 88 | \\INSERT INTO zdb_test (first_name, age) 89 | \\VALUES (?, ?) 90 | , 91 | [_]ZdbTest{ .{ .first_name = "Joe", .age = 20 }, .{ .first_name = "Jane", .age = 35 } }, 92 | ); 93 | 94 | // Another option is to use a tuple - this is a nice demonstration of how the parameter binding is positional, not related to the 95 | // field names. Here we create an explicit tuple type in order to properly type our param array 96 | const InsertTuple = std.meta.Tuple(&.{ []const u8, u32 }); 97 | _ = try cursor.insert( 98 | allocator, 99 | \\INSERT INTO zdb_test (first_name, age) 100 | \\VALUES (?, ?) 101 | , 102 | [_]InsertTuple{ 103 | .{ "GiGi", 85 }, 104 | }, 105 | ); 106 | 107 | // If you are only binding one parameter in the insert query, you don't have to create a struct/tuple to pass params. Single params can be passed 108 | // as an array/slice and they'll be inserted one-by-one. 109 | 110 | // Now that the data's been inserted, we can query the table. 111 | // 112 | // Here we're finally going to see an example of using the ResultSet return value from executeDirect. We can also see that we're binding a parameter 113 | // to this query. If this was a prepared statement, we could execute it multiple times with different parameters. 114 | // 115 | // We're selecting only first_name and age from the table so that the result set can match ZdbTest, however it's important to note that, 116 | // just like with insert, these bindings are *positional*, not name-based. If we reversed then order of these columns in the result set 117 | // then we would get an incorrect binding. 118 | var result_set = try cursor.executeDirect(allocator, "select first_name, age from zdb_test where age < ?", .{40}); 119 | 120 | // ResultSet has two ways of fetching results from the DB - ItemIterator and RowIterator. In this example we're using ItemIterator. This 121 | // will bind rows to structs, allowing you to directly convert query results to Zig data types. This is useful if you know what columns you'll 122 | // be extracting ahead of time and can design structs around your queries (or vice versa). 123 | // 124 | // RowIterator is useful for binding to arbitrary columns which you can then query on an individual basis. This might come in handy if you don't 125 | // know what queries are going to be run, or if a query is going to be returning large numbers of columns and you only want to look at a few of them 126 | // without having to model the entire result set. 127 | // 128 | // Both types of iterators are used in the same way - get a result set, call a function to get an iterator over the results, and then call iter.next() to 129 | // get each row in the format specified by the type of iterator. 130 | var result_iter = try result_set.itemIterator(ZdbTest, allocator); 131 | defer result_iter.deinit(); 132 | 133 | while (try result_iter.next()) |item| { 134 | // Since we're getting ZdbTest's out of this iterator, we get to directly access our data through struct fields. 135 | std.debug.print("First Name: {s}\n", .{item.first_name}); 136 | std.debug.print("Age: {}\n", .{item.age}); 137 | } 138 | } 139 | 140 | { 141 | try conn.connectWithConfig(allocator, basic_connect_config); 142 | 143 | var cursor = try conn.getCursor(allocator); 144 | defer cursor.deinit(allocator) catch {}; 145 | 146 | _ = cursor.executeDirect(allocator, "DROP DATABASE create_example", .{}) catch |err| { 147 | std.debug.print("Error dropping database: {}\n", .{err}); 148 | const recs = cursor.getErrors(allocator); 149 | defer allocator.free(recs); 150 | for (recs) |rec| { 151 | std.debug.print("Message: {s}\n", .{rec.error_message}); 152 | } 153 | return err; 154 | }; 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /examples/04_row_binding.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Allocator = std.mem.Allocator; 3 | 4 | const zdb = @import("zdb"); 5 | const Connection = zdb.Connection; 6 | 7 | pub fn main() anyerror!void { 8 | const allocator = std.heap.page_allocator; 9 | 10 | // This example is the same as create_and_query_table, except we're going to see how to use RowIterator to fetch results instead of 11 | // ItemIterator. 12 | var basic_connect_config = Connection.ConnectionConfig{ 13 | .driver = "PostgreSQL Unicode(x64)", 14 | .database = "postgres", 15 | .server = "localhost", 16 | .port = "5433", 17 | .username = "postgres", 18 | .password = "postgres", 19 | }; 20 | 21 | var conn = try Connection.init(.{}); 22 | defer conn.deinit(); 23 | 24 | { 25 | try conn.connectWithConfig(allocator, basic_connect_config); 26 | defer conn.disconnect(); 27 | 28 | var cursor = try conn.getCursor(allocator); 29 | defer cursor.deinit(allocator) catch {}; 30 | 31 | var rs = try cursor.executeDirect(allocator, "SELECT * FROM pg_database WHERE datname = 'create_example'", .{}); 32 | 33 | var results = try rs.rowIterator(allocator); 34 | 35 | const table_exists = if (results.next()) |value| value != null else |_| false; 36 | 37 | if (table_exists) { 38 | _ = try cursor.executeDirect(allocator, "DROP DATABASE create_example", .{}); 39 | } 40 | } 41 | 42 | { 43 | try conn.connectWithConfig(allocator, basic_connect_config); 44 | defer conn.disconnect(); 45 | 46 | var cursor = try conn.getCursor(allocator); 47 | defer cursor.deinit(allocator) catch {}; 48 | 49 | // try createOrReplaceDb(allocator, &cursor, "create_example"); 50 | _ = try cursor.executeDirect(allocator, "CREATE DATABASE create_example WITH OWNER = postgres", .{}); 51 | } 52 | 53 | var db_connect_config = basic_connect_config; 54 | db_connect_config.database = "create_example"; 55 | 56 | { 57 | try conn.connectWithConfig(allocator, db_connect_config); 58 | defer conn.disconnect(); 59 | 60 | var cursor = try conn.getCursor(allocator); 61 | defer cursor.deinit(allocator) catch {}; 62 | 63 | _ = try cursor.executeDirect(allocator, 64 | \\CREATE TABLE zdb_test 65 | \\( 66 | \\ id serial primary key, 67 | \\ first_name text, 68 | \\ age integer default 0 69 | \\) 70 | , .{}); 71 | 72 | const ZdbTest = struct { 73 | first_name: []const u8, 74 | age: u32, 75 | }; 76 | 77 | _ = try cursor.insert( 78 | allocator, 79 | \\INSERT INTO zdb_test (first_name, age) 80 | \\VALUES (?, ?) 81 | , 82 | [_]ZdbTest{ 83 | .{ .first_name = "Joe", .age = 20 }, 84 | .{ .first_name = "Jane", .age = 35 }, 85 | .{ .first_name = "GiGi", .age = 85 }, 86 | }, 87 | ); 88 | 89 | var result_set = try cursor.executeDirect(allocator, "select * from zdb_test where age < ?", .{40}); 90 | 91 | // Here's where the example starts to be meaningfully different than 03 - when getting the results from the ResultSet, 92 | // we use rowIterator instead of itemIterator. Notice that there's no need to specify a type here. All results will be 93 | // mapped to the built in ResultSet.Row type. 94 | var result_iter = try result_set.rowIterator(allocator); 95 | defer result_iter.deinit(allocator); 96 | 97 | // Just like with ItemIterator we can iterate over all of the results. 98 | while (try result_iter.next()) |row| { 99 | // Instead of being able to get data as fields, we have to specify a name and data type in the "get" function 100 | const id = try row.get(u32, "id"); 101 | // Columns can also be fetched by index. Column indicies are 1-based. 102 | const first_name = try row.getWithIndex([]const u8, 2); 103 | 104 | std.debug.print("{}: {s}\n", .{ id, first_name }); 105 | 106 | // The main use of RowIterator is for getting result sets from unknown queries, where you can't specify 107 | // a struct type to use ahead of time. This means that it's also very likely that you won't know what 108 | // data types to use when extracting column values! For that use case there's Row.printColumn(). 109 | // 110 | // printColumn writes whatever value was fetched as a string using a user-defined writer. You can specify 111 | // custom format strings based on the SQLType of the column, which can be checked on the Row struct. There are default 112 | // format strings defined for all SQLTypes already, so in most cases you don't have to specify anything and the data should 113 | // print in a predictable way 114 | var stdout_writer = std.io.getStdOut().writer(); 115 | try row.printColumn("age", .{}, stdout_writer); 116 | 117 | std.debug.print("\n", .{}); 118 | 119 | // Just as an example, let's specify some format options. We'll add a prefix and print the age as a hex value 120 | // We'll also print the age column using its index rather than the column name 121 | try row.printColumnAtIndex(3, .{ .integer = "Hex Value: {x}\n" }, stdout_writer); 122 | } 123 | } 124 | 125 | { 126 | try conn.connectWithConfig(allocator, db_connect_config); 127 | defer conn.disconnect(); 128 | 129 | var cursor = try conn.getCursor(allocator); 130 | defer cursor.deinit(allocator) catch {}; 131 | 132 | _ = cursor.executeDirect(allocator, "DROP DATABASE create_example", .{}) catch { 133 | const errors = cursor.getErrors(allocator); 134 | defer allocator.free(errors); 135 | 136 | for (errors) |e| { 137 | std.debug.print("Error: {s}\n", .{e.error_message}); 138 | } 139 | }; 140 | } 141 | } 142 | 143 | // pub fn createOrReplaceDb(allocator: Allocator, cursor: *zdb.Cursor, table_name: []const u8) !void { 144 | // var rs = try cursor.executeDirect(allocator, "SELECT * FROM pg_database WHERE datname = ?", .{table_name}); 145 | 146 | // var results = try rs.rowIterator(allocator); 147 | 148 | // const table_exists = if (results.next()) |value| value != null else |_| false; 149 | 150 | // if (table_exists) { 151 | // // NEVER DO THIS IN REAL CODE 152 | // const statement = try std.fmt.allocPrint(allocator, "DROP DATABASE {s}", .{table_name}); 153 | // defer allocator.free(statement); 154 | 155 | // _ = try cursor.executeDirect(allocator, statement, .{}); 156 | // } 157 | 158 | // _ = try cursor.executeDirect(allocator, "CREATE DATABASE create_example WITH OWNER = postgres", .{}); 159 | // } 160 | -------------------------------------------------------------------------------- /src/Connection.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Allocator = std.mem.Allocator; 3 | 4 | const odbc = @import("odbc"); 5 | 6 | const Cursor = @import("Cursor.zig"); 7 | 8 | pub const CommitMode = enum(u1) { auto, manual }; 9 | 10 | const Connection = @This(); 11 | 12 | pub const ConnectionConfig = struct { 13 | driver: ?[]const u8 = null, 14 | dsn: ?[]const u8 = null, 15 | database: ?[]const u8 = null, 16 | server: ?[]const u8 = null, 17 | port: ?[]const u8 = null, 18 | username: ?[]const u8 = null, 19 | password: ?[]const u8 = null, 20 | 21 | pub fn getConnectionString(config: ConnectionConfig, allocator: Allocator) ![]const u8 { 22 | var buffer = std.ArrayList(u8).init(allocator); 23 | errdefer buffer.deinit(); 24 | 25 | var string_builder = buffer.writer(); 26 | 27 | // try string_builder.writeAll("ODBC;"); 28 | 29 | if (config.driver) |driver| try string_builder.print("DRIVER={{{s}}};", .{driver}); 30 | if (config.dsn) |dsn| try string_builder.print("DSN={s};", .{dsn}); 31 | if (config.database) |database| try string_builder.print("DATABASE={s};", .{database}); 32 | if (config.server) |server| try string_builder.print("SERVER={s};", .{server}); 33 | if (config.port) |port| try string_builder.print("PORT={s};", .{port}); 34 | if (config.username) |username| try string_builder.print("UID={s};", .{username}); 35 | if (config.password) |password| try string_builder.print("PWD={s};", .{password}); 36 | 37 | return buffer.toOwnedSlice(); 38 | } 39 | }; 40 | 41 | pub const ConnectionOptions = struct { 42 | version: odbc.Types.EnvironmentAttributeValue.OdbcVersion = .Odbc3, 43 | }; 44 | 45 | environment: odbc.Environment, 46 | connection: odbc.Connection, 47 | 48 | /// Initialize a new Connection instance with the given ODBC version. The default version is 3.0. 49 | /// This does not connect to a database. 50 | pub fn init(config: ConnectionOptions) !Connection { 51 | var connection: Connection = undefined; 52 | 53 | connection.environment = try odbc.Environment.init(); 54 | errdefer connection.environment.deinit() catch {}; 55 | 56 | try connection.environment.setOdbcVersion(config.version); 57 | 58 | connection.connection = try odbc.Connection.init(&connection.environment); 59 | 60 | return connection; 61 | } 62 | 63 | /// Connect to a database using a server/host, username, and password. 64 | pub fn connect(conn: *Connection, server_name: []const u8, username: []const u8, password: []const u8) !void { 65 | try conn.connection.connect(server_name, username, password); 66 | } 67 | 68 | /// Connect to a database using a ConnectionConfig. The config will be converted to a connection string, and then 69 | /// it will attempt to connect using connectExtended. 70 | pub fn connectWithConfig(conn: *Connection, allocator: Allocator, connection_config: ConnectionConfig) !void { 71 | var connection_string = try connection_config.getConnectionString(allocator); 72 | defer allocator.free(connection_string); 73 | 74 | try conn.connection.connectExtended(connection_string, .NoPrompt); 75 | } 76 | 77 | /// Connect using a pre-created connection string. 78 | pub fn connectExtended(conn: *Connection, connection_string: []const u8) !void { 79 | try conn.connection.connectExtended(connection_string, .NoPrompt); 80 | } 81 | 82 | /// Close the connection and environment handles. Does not disconnect from the data source. 83 | pub fn deinit(self: *Connection) void { 84 | self.connection.deinit() catch {}; 85 | self.environment.deinit() catch {}; 86 | } 87 | 88 | /// Disconnect from the currently connected data source. 89 | pub fn disconnect(self: *Connection) void { 90 | self.connection.disconnect() catch {}; 91 | } 92 | 93 | /// Sets the commit mode which will be used in all cursors created from this connection. 94 | /// If the commit mode is `auto`, then each statement execution will be immediately committed. If the 95 | /// commit mode is `manual` then statement executions won't be commited until the user calls `cursor.commit()`. 96 | /// This can be used for transaction management. 97 | pub fn setCommitMode(self: *Connection, mode: CommitMode) !void { 98 | try self.connection.setAttribute(.{ .Autocommit = mode == .auto }); 99 | } 100 | 101 | /// Get a new cursor, with which you can execute SQL statements. 102 | pub fn getCursor(self: *Connection, allocator: Allocator) !Cursor { 103 | return try Cursor.init(allocator, self.connection); 104 | } 105 | 106 | test "ConnectionInfo" { 107 | const allocator = std.testing.allocator; 108 | 109 | var connection_info = ConnectionConfig{ 110 | .driver = "A Driver", 111 | .dsn = "Some DSN Value", 112 | .username = "User", 113 | .password = "Password", 114 | }; 115 | 116 | const connection_string = try connection_info.getConnectionString(allocator); 117 | defer allocator.free(connection_string); 118 | 119 | try std.testing.expectEqualStrings("DRIVER=A Driver;DSN=Some DSN Value;UID=User;PWD=Password", connection_string); 120 | } 121 | -------------------------------------------------------------------------------- /src/Cursor.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Allocator = std.mem.Allocator; 3 | 4 | const odbc = @import("odbc"); 5 | const Statement = odbc.Statement; 6 | const Connection = odbc.Connection; 7 | 8 | const ParameterBucket = @import("ParameterBucket.zig"); 9 | const ResultSet = @import("ResultSet.zig"); 10 | 11 | const catalog_types = @import("catalog.zig"); 12 | const Column = catalog_types.Column; 13 | const Table = catalog_types.Table; 14 | const TablePrivileges = catalog_types.TablePrivileges; 15 | 16 | const Cursor = @This(); 17 | 18 | parameters: ParameterBucket, 19 | 20 | connection: Connection, 21 | statement: Statement, 22 | 23 | pub fn init(allocator: Allocator, connection: Connection) !Cursor { 24 | return Cursor{ 25 | .connection = connection, 26 | .statement = try Statement.init(connection), 27 | .parameters = try ParameterBucket.init(allocator, 10), 28 | }; 29 | } 30 | 31 | pub fn deinit(self: *Cursor, allocator: Allocator) !void { 32 | try self.close(); 33 | try self.statement.deinit(); 34 | self.parameters.deinit(allocator); 35 | } 36 | 37 | /// Close the current cursor. If the cursor is not open, does nothing and does not return an error. 38 | pub fn close(self: *Cursor) !void { 39 | self.statement.closeCursor() catch |err| switch (err) { 40 | error.InvalidCursorState => return, 41 | else => return err, 42 | }; 43 | } 44 | 45 | /// Execute a SQL statement and return the result set. SQL query parameters can be passed with the `parameters` argument. 46 | /// This is the fastest way to execute a SQL statement once. 47 | pub fn executeDirect(cursor: *Cursor, allocator: Allocator, sql_statement: []const u8, parameters: anytype) !ResultSet { 48 | var num_params: usize = 0; 49 | for (sql_statement) |c| { 50 | if (c == '?') num_params += 1; 51 | } 52 | 53 | if (num_params != parameters.len) return error.InvalidNumParams; 54 | try cursor.parameters.reset(allocator, num_params); 55 | 56 | try cursor.bindParameters(allocator, parameters); 57 | try cursor.statement.executeDirect(sql_statement); 58 | 59 | return ResultSet.init(cursor.statement); 60 | } 61 | 62 | /// Execute a statement and return the result set. A statement must have been prepared previously 63 | /// using `Cursor.prepare()`. 64 | pub fn execute(cursor: *Cursor) !ResultSet { 65 | try cursor.statement.execute(); 66 | return try ResultSet.init(cursor.statement); 67 | } 68 | 69 | /// Prepare a SQL statement for execution. If you want to execute a statement multiple times, 70 | /// preparing it first is much faster because you only have to compile and load the statement 71 | /// once on the driver/DBMS. Use `Cursor.execute()` to get the results. 72 | /// 73 | /// If you don't want to set the paramters here, that's fine. You can pass `.{}` and use `cursor.bindParameter` or 74 | /// `cursor.bindParameters` later before executing the statement. 75 | pub fn prepare(cursor: *Cursor, allocator: Allocator, sql_statement: []const u8, parameters: anytype) !void { 76 | try cursor.bindParameters(allocator, parameters); 77 | try cursor.statement.prepare(sql_statement); 78 | } 79 | 80 | /// `insert` is a helper function for inserting data. It will prepare the statement you provide and then 81 | /// execute it once for each value in the `values` array. It's not fundamentally different than this: 82 | /// 83 | /// ```zig 84 | /// const values = [_][2]u32{ [_]u32{0, 1}, [_]u32{1, 2}, [_]u32{2, 3}, [_]u32{3, 4}, }; 85 | /// 86 | /// try cursor.prepare(allocator, "INSERT INTO table VALUES (?, ?)"); 87 | /// for (values) |v| { 88 | /// try cursor.bindParameters(allocator, v); 89 | /// try cursor.execute(); 90 | /// } 91 | /// ``` 92 | /// 93 | /// `insert` will do some work to make sure that each value binds in the appropriate way regardless 94 | /// of its type. 95 | /// 96 | /// `insert` is named as such because it's intended to be used when inserting data, however there is nothing 97 | /// here requiring you to use this for insert statements. You could use it for other things, like regular 98 | /// SELECT statements. It's important to keep in mind, though, that there will be no way to fetch result 99 | /// sets for any query other than the last. 100 | pub fn insert(cursor: *Cursor, allocator: Allocator, insert_statement: []const u8, values: anytype) !usize { 101 | // @todo Try using arrays of parameters for bulk ops 102 | const DataType = switch (@typeInfo(@TypeOf(values))) { 103 | .Pointer => |info| switch (info.size) { 104 | .Slice => info.child, 105 | else => @compileError("values must be a slice or array type"), 106 | }, 107 | .Array => |info| info.child, 108 | else => @compileError("values must be a slice or array type"), 109 | }; 110 | AssertInsertable(DataType); 111 | 112 | const num_params = blk: { 113 | var count: usize = 0; 114 | for (insert_statement) |c| { 115 | if (c == '?') count += 1; 116 | } 117 | break :blk count; 118 | }; 119 | 120 | try cursor.parameters.reset(allocator, num_params); 121 | try cursor.prepare(allocator, insert_statement, .{}); 122 | 123 | var num_rows_inserted: usize = 0; 124 | 125 | for (values, 0..) |value, value_index| { 126 | switch (@typeInfo(DataType)) { 127 | .Pointer => |pointer_tag| switch (pointer_tag.size) { 128 | .Slice => { 129 | if (value.len < num_params) return error.WrongParamCount; 130 | for (value, 0..) |param, param_index| { 131 | try cursor.bindParameter(allocator, param_index + 1, param); 132 | } 133 | }, 134 | .One => { 135 | if (num_params != 1) return error.WrongParamCount; 136 | try cursor.bindParameter(allocator, value_index + 1, value.*); 137 | }, 138 | else => unreachable, 139 | }, 140 | .Struct => |struct_tag| { 141 | if (struct_tag.fields.len != num_params) return error.WrongParamCount; 142 | 143 | inline for (std.meta.fields(DataType), 0..) |field, param_index| { 144 | try cursor.bindParameter(allocator, param_index + 1, @field(value, field.name)); 145 | } 146 | }, 147 | .Array => |array_tag| { 148 | if (array_tag.len != num_params) return error.WrongParamCount; 149 | for (value, 0..) |val, param_index| { 150 | try cursor.bindParameter(allocator, param_index + 1, val); 151 | } 152 | }, 153 | .Optional => { 154 | if (num_params != 1) return error.WrongParamCount; 155 | try cursor.bindParameter(allocator, value_index + 1, value); 156 | }, 157 | .Enum => { 158 | if (num_params != 1) return error.WrongParamCount; 159 | const enum_value = @intFromEnum(value); 160 | try cursor.bindParameter(allocator, value_index + 1, enum_value); 161 | }, 162 | .EnumLiteral => { 163 | if (num_params != 1) return error.WrongParamCount; 164 | try cursor.bindParameter(allocator, value_index + 1, @tagName(value)); 165 | }, 166 | .Int, .Float, .ComptimeInt, .ComptimeFloat, .Bool => { 167 | if (num_params != 1) return error.WrongParamCount; 168 | try cursor.bindParameter(allocator, value_index + 1, value); 169 | }, 170 | else => unreachable, 171 | } 172 | 173 | cursor.statement.execute() catch |err| { 174 | std.log.err("{s}", .{@errorName(err)}); 175 | return err; 176 | }; 177 | num_rows_inserted += try cursor.statement.rowCount(); 178 | } 179 | 180 | return num_rows_inserted; 181 | } 182 | 183 | /// When in manual-commit mode, use this to commit a transaction. **Important!:** This will 184 | /// commit *all open cursors allocated on this connection*. Be mindful of that before using 185 | /// this, if in a situation where you are using multiple cursors simultaneously. 186 | pub fn commit(self: *Cursor) !void { 187 | try self.connection.endTransaction(.commit); 188 | } 189 | 190 | /// When in manual-commit mode, use this to rollback a transaction. **Important!:** This will 191 | /// rollback *all open cursors allocated on this connection*. Be mindful of that before using 192 | /// this, if in a situation where you are using multiple cursors simultaneously. 193 | pub fn rollback(self: *Cursor) !void { 194 | try self.connection.endTransaction(.rollback); 195 | } 196 | 197 | pub fn columns(cursor: *Cursor, allocator: Allocator, catalog_name: ?[]const u8, schema_name: ?[]const u8, table_name: []const u8) ![]Column { 198 | try cursor.statement.columns(catalog_name, schema_name, table_name, null); 199 | var result_set = ResultSet.init(cursor.statement); 200 | 201 | var column_iter = try result_set.itemIterator(Column, allocator); 202 | defer column_iter.deinit(); 203 | 204 | var column_result = std.ArrayList(Column).init(allocator); 205 | errdefer column_result.deinit(); 206 | 207 | while (true) { 208 | var result = column_iter.next() catch continue; 209 | var column = result orelse break; 210 | try column_result.append(column); 211 | } 212 | 213 | return column_result.toOwnedSlice(); 214 | } 215 | 216 | pub fn tables(cursor: *Cursor, allocator: Allocator, catalog_name: ?[]const u8, schema_name: ?[]const u8) ![]Table { 217 | try cursor.statement.tables(catalog_name, schema_name, null, null); 218 | var result_set = ResultSet.init(allocator, cursor.statement); 219 | 220 | var table_iter = try result_set.itemIterator(Table); 221 | defer table_iter.deinit(); 222 | 223 | var table_result = std.ArrayList(Table).init(allocator); 224 | errdefer table_result.deinit(); 225 | 226 | while (true) { 227 | var result = table_iter.next() catch continue; 228 | var table = result orelse break; 229 | try table_result.append(table); 230 | } 231 | 232 | return table_result.toOwnedSlice(); 233 | } 234 | 235 | pub fn tablePrivileges(cursor: *Cursor, allocator: Allocator, catalog_name: ?[]const u8, schema_name: ?[]const u8, table_name: []const u8) ![]TablePrivileges { 236 | try cursor.statement.tablePrivileges(catalog_name, schema_name, table_name); 237 | var result_set = ResultSet.init(cursor.statement); 238 | 239 | var priv_iter = try result_set.itemIterator(TablePrivileges, allocator); 240 | defer priv_iter.deinit(); 241 | 242 | var priv_result = std.ArrayList(TablePrivileges).init(allocator); 243 | errdefer priv_result.deinit(); 244 | 245 | while (true) { 246 | var result = priv_iter.next() catch continue; 247 | var privilege = result orelse break; 248 | try priv_result.append(privilege); 249 | } 250 | 251 | return priv_result.toOwnedSlice(); 252 | } 253 | 254 | pub fn catalogs(cursor: *Cursor, allocator: Allocator) ![][]const u8 { 255 | try cursor.statement.getAllCatalogs(); 256 | var catalog_names = std.ArrayList([]const u8).init(allocator); 257 | errdefer catalog_names.deinit(); 258 | 259 | var result_set = ResultSet.init(cursor.statement); 260 | var iter = try result_set.rowIterator(allocator); 261 | defer iter.deinit(allocator); 262 | 263 | while (true) { 264 | var result = iter.next() catch continue; 265 | var row = result orelse break; 266 | const catalog_name = row.get([]const u8, "TABLE_CAT") catch continue; 267 | 268 | const catalog_name_dupe = try allocator.dupe(u8, catalog_name); 269 | try catalog_names.append(catalog_name_dupe); 270 | } 271 | 272 | return catalog_names.toOwnedSlice(); 273 | } 274 | 275 | /// Bind a single value to a SQL parameter. If `self.parameters` is `null`, this does nothing 276 | /// and does not return an error. Parameter indices start at 1. 277 | pub fn bindParameter(cursor: *Cursor, allocator: Allocator, index: usize, parameter: anytype) !void { 278 | const stored_param = try cursor.parameters.set(allocator, parameter, index - 1); 279 | const sql_param = ParameterBucket.SqlParameter.default(parameter); 280 | try cursor.statement.bindParameter( 281 | @as(u16, @intCast(index)), 282 | .Input, 283 | sql_param.c_type, 284 | sql_param.sql_type, 285 | stored_param.data, 286 | sql_param.precision, 287 | stored_param.indicator, 288 | ); 289 | } 290 | 291 | /// Bind a list of parameters to SQL parameters. The first item in the list will be bound 292 | /// to the parameter at index 1, the second to index 2, etc. 293 | /// 294 | /// Calling this function clears all existing parameters, and if an empty list is passed in 295 | /// will not re-initialize them. 296 | pub fn bindParameters(cursor: *Cursor, allocator: Allocator, parameters: anytype) !void { 297 | try cursor.parameters.reset(allocator, parameters.len); 298 | 299 | inline for (parameters, 0..) |param, index| { 300 | try cursor.bindParameter(allocator, index + 1, param); 301 | } 302 | } 303 | 304 | pub fn getErrors(cursor: *Cursor, allocator: Allocator) []odbc.Error.DiagnosticRecord { 305 | return cursor.statement.getDiagnosticRecords(allocator) catch return &[_]odbc.Error.DiagnosticRecord{}; 306 | } 307 | 308 | /// Assert that the type `T` can be used as an insert parameter. Deeply checks types that have child types when necessary. 309 | fn AssertInsertable(comptime T: type) void { 310 | switch (@typeInfo(T)) { 311 | .Frame, .AnyFrame, .Void, .NoReturn, .Undefined, .ErrorUnion, .ErrorSet, .Fn, .Union, .Vector, .Opaque, .Null, .Type => @compileError(@tagName(std.meta.activeTag(@typeInfo(T))) ++ " types cannot be used as insert parameters"), 312 | .Pointer => |pointer_tag| switch (pointer_tag.size) { 313 | .Slice, .One => AssertInsertable(pointer_tag.child), 314 | else => @compileError(@tagName(std.meta.activeTag(pointer_tag.size)) ++ "-type pointers cannot be used as insert parameters"), 315 | }, 316 | .Array => |array_tag| AssertInsertable(array_tag.child), 317 | .Optional => |op_tag| AssertInsertable(op_tag.child), 318 | .Struct => {}, 319 | .Enum, .EnumLiteral => {}, 320 | .Int, .Float, .ComptimeInt, .ComptimeFloat, .Bool => {}, 321 | } 322 | } 323 | -------------------------------------------------------------------------------- /src/ParameterBucket.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Allocator = std.mem.Allocator; 3 | const odbc = @import("odbc"); 4 | 5 | const EraseComptime = @import("util.zig").EraseComptime; 6 | 7 | const ParameterBucket = @This(); 8 | 9 | /// This struct contains the information necessary to communicate with the ODBC driver 10 | /// about the type of a value. `sql_type` is often used to tell the driver how to convert 11 | /// the value into one that SQL will understand, whereas `c_type` is generally used so that 12 | /// the driver has a way to convert a `*anyopaque` or `[]u8` into a value. 13 | pub const SqlParameter = struct { 14 | sql_type: odbc.Types.SqlType, 15 | c_type: odbc.Types.CType, 16 | precision: ?u16 = null, // Only for numeric types, not sure the best way to model this 17 | 18 | /// Get the default SqlType and CType equivalents for an arbitrary value. If the value is a float, 19 | /// precision will be defaulted to `6`. If the value is a `comptime_int` or `comptime_float`, then 20 | /// it will be converted here to `i64` or `f64`, respectively. 21 | pub fn default(value: anytype) SqlParameter { 22 | const ValueType = EraseComptime(@TypeOf(value)); 23 | 24 | return SqlParameter{ 25 | .sql_type = comptime odbc.Types.SqlType.fromType(ValueType) orelse @compileError("Cannot get default SqlType for type " ++ @typeName(ValueType)), 26 | .c_type = comptime odbc.Types.CType.fromType(ValueType) orelse @compileError("Cannot get default CType for type " ++ @typeName(ValueType)), 27 | .precision = if (std.meta.trait.isFloat(@TypeOf(value))) 6 else null, 28 | }; 29 | } 30 | }; 31 | 32 | // @todo I think that it's actually going to be beneficial to return to the original design of ParameterBucket which used a []u8 to hold 33 | // all of the param data. That idea wasn't the problem, the problem is that I didn't work hard enough to build a system that can properly 34 | // realloc/move data when the user wants to replace values. 35 | // 36 | // This design actually has a big disadvantage which is that every parameter that gets set needs to be separately allocated in order to produce 37 | // a *anyopaque to store in Param. With the []u8 approach, extra space will need to be reallocated much more rarely since the data will be stored 38 | // in the same persistent buffer. 39 | pub const Param = struct { 40 | data: *anyopaque, 41 | indicator: *c_longlong, 42 | }; 43 | 44 | data: []u8, 45 | indicators: []c_longlong, 46 | 47 | pub fn init(allocator: Allocator, num_params: usize) !ParameterBucket { 48 | var indicators = try allocator.alloc(c_longlong, num_params); 49 | errdefer allocator.free(indicators); 50 | 51 | for (indicators) |*i| i.* = 0; 52 | 53 | return ParameterBucket{ 54 | .data = try allocator.alloc(u8, num_params * 8), 55 | .indicators = indicators, 56 | }; 57 | } 58 | 59 | pub fn deinit(bucket: *ParameterBucket, allocator: Allocator) void { 60 | allocator.free(bucket.data); 61 | allocator.free(bucket.indicators); 62 | } 63 | 64 | pub fn reset(bucket: *ParameterBucket, allocator: Allocator, new_param_count: usize) !void { 65 | if (new_param_count > bucket.indicators.len) { 66 | bucket.indicators = try allocator.realloc(bucket.indicators, new_param_count); 67 | bucket.data = try allocator.realloc(bucket.data, new_param_count * 8); 68 | } 69 | 70 | for (bucket.indicators) |*i| i.* = 0; 71 | for (bucket.data) |*d| d.* = 0; 72 | } 73 | 74 | pub fn set(bucket: *ParameterBucket, allocator: Allocator, param_data: anytype, param_index: usize) !Param { 75 | const ParamType = EraseComptime(@TypeOf(param_data)); 76 | 77 | var data_index: usize = 0; 78 | for (bucket.indicators[0..param_index]) |indicator| { 79 | data_index += @as(usize, @intCast(indicator)); 80 | } 81 | 82 | const data_indicator = @as(usize, @intCast(bucket.indicators[param_index])); 83 | 84 | const data_buffer: []const u8 = if (comptime std.meta.trait.isZigString(ParamType)) 85 | param_data 86 | else 87 | &std.mem.toBytes(@as(ParamType, param_data)); 88 | 89 | bucket.indicators[param_index] = @as(c_longlong, @intCast(data_buffer.len)); 90 | 91 | if (data_buffer.len != data_indicator) { 92 | // If the new len is not the same as the old one, then some adjustments have to be made to the rest of 93 | // the params 94 | var remaining_param_size: usize = 0; 95 | for (bucket.indicators[param_index..]) |ind| { 96 | remaining_param_size += @as(usize, @intCast(ind)); 97 | } 98 | 99 | const original_data_end_index = data_index + data_indicator; 100 | const new_data_end_index = data_index + data_buffer.len; 101 | 102 | const copy_dest = bucket.data[new_data_end_index..]; 103 | const copy_src = bucket.data[original_data_end_index .. original_data_end_index + remaining_param_size]; 104 | 105 | if (data_buffer.len < data_indicator) { 106 | // If the new len is smaller than the old one, then just move the remaining params 107 | // forward 108 | std.mem.copy(u8, copy_dest, copy_src); 109 | } else { 110 | // If the new len is bigger than the old one, then resize the buffer and then move the 111 | // remaining params backwards 112 | const size_increase = data_buffer.len - data_indicator; 113 | bucket.data = try allocator.realloc(bucket.data, bucket.data.len + size_increase); 114 | 115 | std.mem.copyBackwards(u8, copy_dest, copy_src); 116 | } 117 | } 118 | 119 | std.mem.copy(u8, bucket.data[data_index..], data_buffer); 120 | 121 | return Param{ 122 | .data = @as(*anyopaque, @ptrCast(&bucket.data[data_index])), 123 | .indicator = &bucket.indicators[param_index], 124 | }; 125 | } 126 | 127 | test "SqlParameter defaults" { 128 | const SqlType = odbc.Types.SqlType; 129 | const CType = odbc.Types.CType; 130 | const a = SqlParameter.default(10); 131 | 132 | try std.testing.expect(a.precision == null); 133 | try std.testing.expectEqual(CType.SBigInt, a.c_type); 134 | try std.testing.expectEqual(SqlType.BigInt, a.sql_type); 135 | } 136 | 137 | test "SqlParameter string" { 138 | const SqlType = odbc.Types.SqlType; 139 | const CType = odbc.Types.CType; 140 | const param = SqlParameter.default("some string"); 141 | 142 | try std.testing.expect(param.precision == null); 143 | try std.testing.expectEqual(CType.Char, param.c_type); 144 | try std.testing.expectEqual(SqlType.Varchar, param.sql_type); 145 | } 146 | 147 | test "add parameter to ParameterBucket" { 148 | const allocator = std.testing.allocator; 149 | 150 | var bucket = try ParameterBucket.init(allocator, 5); 151 | defer bucket.deinit(allocator); 152 | 153 | var param_value: u32 = 10; 154 | 155 | const param = try bucket.set(allocator, param_value, 0); 156 | 157 | const param_data = @as([*]u8, @ptrCast(param.data))[0..@as(usize, @intCast(param.indicator.*))]; 158 | try std.testing.expectEqualSlices(u8, std.mem.toBytes(param_value)[0..], param_data); 159 | } 160 | 161 | test "add string parameter to ParameterBucket" { 162 | const allocator = std.testing.allocator; 163 | 164 | var bucket = try ParameterBucket.init(allocator, 5); 165 | defer bucket.deinit(allocator); 166 | 167 | var param_value = "some string value"; 168 | 169 | const param = try bucket.set(allocator, param_value, 0); 170 | 171 | const param_data = @as([*]u8, @ptrCast(param.data))[0..@as(usize, @intCast(param.indicator.*))]; 172 | try std.testing.expectEqualStrings(param_value, param_data); 173 | } 174 | -------------------------------------------------------------------------------- /src/ResultSet.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Allocator = std.mem.Allocator; 3 | 4 | const odbc = @import("odbc"); 5 | const RowStatus = odbc.Types.StatementAttributeValue.RowStatus; 6 | const Statement = odbc.Statement; 7 | const CType = odbc.Types.CType; 8 | 9 | const util = @import("util.zig"); 10 | const sliceToValue = util.sliceToValue; 11 | 12 | const log = std.log.scoped(.result_set); 13 | 14 | const ResultSet = @This(); 15 | 16 | /// Given a struct, generate a new struct that can be used for ODBC row-wise binding. The conversion goes 17 | /// roughly like this; 18 | /// ``` 19 | /// const Base = struct { 20 | /// field1: u32, 21 | /// field2: []const u8, 22 | /// field3: ?[]const u8 23 | /// }; 24 | /// 25 | /// // Becomes.... 26 | /// 27 | /// const FetchResult(Base).RowType = extern struct { 28 | /// field1: u32, 29 | /// field1_len_or_ind: c_longlong, 30 | /// field2: [200]u8, 31 | /// field2_len_or_ind: c_longlong, 32 | /// field3: [200]u8, 33 | /// field3_len_or_ind: c_longlong 34 | /// }; 35 | /// ``` 36 | pub fn FetchResult(comptime Target: type) type { 37 | const Type = std.builtin.Type; 38 | const TargetInfo = @typeInfo(Target); 39 | 40 | if (TargetInfo != .Struct) { 41 | @compileError("The base type of FetchResult must be a struct, found " ++ @typeName(Target)); 42 | } 43 | 44 | const R = extern struct {}; 45 | var ResultInfo = @typeInfo(R); 46 | 47 | var result_fields: [TargetInfo.Struct.fields.len * 2]Type.StructField = undefined; 48 | inline for (TargetInfo.Struct.fields, 0..) |field, i| { 49 | // Initialize all the fields of the StructField 50 | result_fields[i * 2] = field; 51 | 52 | // Get the target type of the generated struct 53 | const field_type_info = @typeInfo(field.type); 54 | const column_type = if (field_type_info == .Optional) field_type_info.Optional.child else field.type; 55 | const column_field_type = switch (@typeInfo(column_type)) { 56 | .Pointer => |info| switch (info.size) { 57 | .Slice => [200]info.child, 58 | else => column_type, 59 | }, 60 | .Enum => |info| info.tag_type, 61 | else => column_type, 62 | }; 63 | 64 | // Reset the field_type and default_value to be whatever was calculated 65 | // (default value is reset to null because it has to be a null of the correct type) 66 | result_fields[i * 2].type = column_field_type; 67 | result_fields[i * 2].default_value = null; 68 | // Generate the len_or_ind field to coincide with the main column field 69 | result_fields[(i * 2) + 1] = Type.StructField{ .name = field.name ++ "_len_or_ind", .type = i64, .default_value = null, .is_comptime = false, .alignment = @alignOf(c_longlong) }; 70 | } 71 | 72 | ResultInfo.Struct.fields = result_fields[0..]; 73 | 74 | return @Type(ResultInfo); 75 | } 76 | 77 | fn toTarget(comptime Target: type, allocator: Allocator, row: FetchResult(Target)) error{ InvalidNullValue, OutOfMemory }!Target { 78 | var item: Target = undefined; 79 | inline for (std.meta.fields(Target)) |field| { 80 | @setEvalBranchQuota(1_000_000); 81 | const row_data = @field(row, field.name); 82 | const len_or_indicator = @field(row, field.name ++ "_len_or_ind"); 83 | 84 | const field_type_info = @typeInfo(field.type); 85 | if (len_or_indicator == odbc.sys.SQL_NULL_DATA) { 86 | // Handle null data. For Optional types, set the field to null. For non-optional types with 87 | // a default value given, set the field to the default value. For all others, return 88 | // an error 89 | if (field_type_info == .Optional) { 90 | @field(item, field.name) = null; 91 | } else if (field.default_value) |default| { 92 | @field(item, field.name) = default; 93 | } else { 94 | return error.InvalidNullValue; 95 | } 96 | } else { 97 | // If the field in Base is optional, we just want to deal with its child type. The possibility of 98 | // the value being null was handled above, so we can assume it's not here 99 | const child_info = if (field_type_info == .Optional) @typeInfo(field_type_info.Optional.child) else field_type_info; 100 | @field(item, field.name) = switch (child_info) { 101 | .Pointer => |info| switch (info.size) { 102 | .Slice => blk: { 103 | // For slices, we want to allocate enough memory to hold the (presumably string) data 104 | // The string length might be indicated by a null byte, or it might be in len_or_indicator. 105 | const slice_length: usize = if (len_or_indicator == odbc.sys.SQL_NTS) 106 | std.mem.indexOf(u8, row_data[0..], &.{0x00}) orelse row_data.len 107 | else 108 | @as(usize, @intCast(len_or_indicator)); 109 | 110 | var data_slice = try allocator.alloc(info.child, slice_length); 111 | std.mem.copy(info.child, data_slice, row_data[0..slice_length]); 112 | break :blk data_slice; 113 | }, 114 | // @warn I've never seen this come up so it might not be strictly necessary, also might be broken 115 | else => row_data, 116 | }, 117 | // Convert enums back from their backing type to the enum value 118 | .Enum => @as(field.type, @enumFromInt(row_data)), 119 | // All other data types can go right back 120 | else => row_data, 121 | }; 122 | } 123 | } 124 | 125 | return item; 126 | } 127 | 128 | /// `Row` represents a single record for `ColumnBindingResultSet`. 129 | pub const Row = struct { 130 | pub const FormatOptions = struct { 131 | char: []const u8 = "{c}", 132 | varchar: []const u8 = "{s}", 133 | long_varchar: []const u8 = "{s}", 134 | w_char: []const u8 = "{u}", 135 | w_varchar: []const u8 = "{u}", 136 | w_long_varchar: []const u8 = "{u}", 137 | decimal: []const u8 = "{d:.5}", 138 | numeric: []const u8 = "{d:.5}", 139 | small_int: []const u8 = "{}", 140 | integer: []const u8 = "{}", 141 | real: []const u8 = "{d:.5}", 142 | float: []const u8 = "{d:.5}", 143 | double: []const u8 = "{d:.5}", 144 | bit: []const u8 = "{}", 145 | tiny_int: []const u8 = "{}", 146 | big_int: []const u8 = "{}", 147 | binary: []const u8 = "{b}", 148 | var_binary: []const u8 = "{b}", 149 | long_var_binary: []const u8 = "{b}", 150 | date: []const u8 = "{}", 151 | time: []const u8 = "{}", 152 | timestamp: []const u8 = "{}", 153 | timestamp_ltz: []const u8 = "{}", 154 | interval_month: []const u8 = "{}", 155 | interval_year: []const u8 = "{}", 156 | interval_year_to_month: []const u8 = "{}", 157 | interval_day: []const u8 = "{}", 158 | interval_hour: []const u8 = "{}", 159 | interval_minute: []const u8 = "{}", 160 | interval_second: []const u8 = "{}", 161 | interval_day_to_hour: []const u8 = "{}", 162 | interval_day_to_minute: []const u8 = "{}", 163 | interval_day_to_second: []const u8 = "{}", 164 | interval_hour_to_minute: []const u8 = "{}", 165 | interval_hour_to_second: []const u8 = "{}", 166 | interval_minute_to_second: []const u8 = "{}", 167 | guid: []const u8 = "{}", 168 | }; 169 | 170 | const Column = struct { 171 | name: []const u8, 172 | data: []u8, 173 | indicator: c_longlong, 174 | sql_type: odbc.Types.SqlType, 175 | 176 | fn isNull(column: Column) bool { 177 | return column.indicator == odbc.sys.SQL_NULL_DATA; 178 | } 179 | 180 | fn getData(column: *const Column) []u8 { 181 | if (column.indicator == odbc.sys.SQL_NTS) { 182 | const null_index = std.mem.indexOf(u8, &.{0}, column.data) orelse column.data.len; 183 | if (null_index > column.data.len) { 184 | return column.data; 185 | } 186 | return column.data[0..null_index]; 187 | } 188 | 189 | return column.data[0..@as(usize, @intCast(column.indicator))]; 190 | } 191 | }; 192 | 193 | columns: []Column, 194 | 195 | fn init(allocator: Allocator, num_columns: usize) !Row { 196 | return Row{ 197 | .columns = try allocator.alloc(Column, num_columns), 198 | }; 199 | } 200 | 201 | fn deinit(self: *Row, allocator: Allocator) void { 202 | allocator.free(self.columns); 203 | } 204 | 205 | /// Get a value from a column using the column name. Will attempt to convert whatever bytes 206 | /// are stored for the column into `ColumnType`. 207 | pub fn get(self: *Row, comptime ColumnType: type, column_name: []const u8) !ColumnType { 208 | const column_index = for (self.columns, 0..) |column, index| { 209 | if (std.mem.eql(u8, column.name, column_name)) break index; 210 | } else return error.ColumnNotFound; 211 | 212 | return try self.getWithIndex(ColumnType, column_index + 1); 213 | } 214 | 215 | /// Get a value from a column using the column index. Column indices start from 1. Will attempt to 216 | /// convert whatever bytes are stored for the column into `ColumnType`. 217 | pub fn getWithIndex(self: *Row, comptime ColumnType: type, column_index: usize) error{InvalidNullValue}!ColumnType { 218 | const target_column = self.columns[column_index - 1]; 219 | 220 | if (target_column.isNull()) { 221 | return switch (@typeInfo(ColumnType)) { 222 | .Optional => null, 223 | else => error.InvalidNullValue, 224 | }; 225 | } 226 | 227 | const column_data = target_column.getData(); 228 | 229 | if (@typeInfo(ColumnType) == .Pointer and @typeInfo(ColumnType).Pointer.size == .Slice) { 230 | return column_data; 231 | } 232 | 233 | return sliceToValue(ColumnType, column_data); 234 | } 235 | 236 | pub fn printColumn(row: *Row, column_name: []const u8, comptime format_options: FormatOptions, writer: anytype) !void { 237 | const column_index = for (row.columns, 0..) |column, index| { 238 | if (std.mem.eql(u8, column.name, column_name)) break index; 239 | } else return error.ColumnNotFound; 240 | 241 | try row.printColumnAtIndex(column_index + 1, format_options, writer); 242 | } 243 | 244 | pub fn printColumnAtIndex(row: *Row, column_index: usize, comptime format_options: FormatOptions, writer: anytype) !void { 245 | const target_column = row.columns[column_index - 1]; 246 | 247 | if (target_column.isNull()) return; 248 | 249 | const column_data = target_column.getData(); 250 | 251 | switch (target_column.sql_type) { 252 | .Char => try writer.print(format_options.char, .{column_data[0]}), 253 | .Varchar => try writer.print(format_options.varchar, .{column_data}), 254 | .LongVarchar => try writer.print(format_options.long_varchar, .{column_data}), 255 | .WChar => try writer.print(format_options.w_char, .{sliceToValue(u16, column_data)}), 256 | .WVarchar => { 257 | const utf8_column_data = sliceToValue([]u16, column_data); 258 | for (utf8_column_data) |wchar| { 259 | try writer.print(format_options.w_varchar, .{wchar}); 260 | } 261 | }, 262 | .WLongVarchar => { 263 | const utf8_column_data = sliceToValue([]u16, column_data); 264 | for (utf8_column_data) |wchar| { 265 | try writer.print(format_options.w_varchar, .{wchar}); 266 | } 267 | }, 268 | .Decimal => try writer.print(format_options.decimal, .{sliceToValue(f32, column_data)}), 269 | .Float => try writer.print(format_options.float, .{sliceToValue(f32, column_data)}), 270 | .Numeric => try writer.print(format_options.numeric, .{sliceToValue(CType.SqlNumeric, column_data).toFloat(f64)}), 271 | .Real => try writer.print(format_options.real, .{sliceToValue(f64, column_data)}), 272 | .Double => try writer.print(format_options.double, .{sliceToValue(f64, column_data)}), 273 | .Bit => try writer.print(format_options.bit, .{column_data[0]}), 274 | .TinyInt => try writer.print(format_options.tiny_int, .{sliceToValue(i8, column_data)}), 275 | .SmallInt => try writer.print(format_options.small_int, .{sliceToValue(i16, column_data)}), 276 | .Integer => try writer.print(format_options.integer, .{sliceToValue(i32, column_data)}), 277 | .BigInt => try writer.print(format_options.big_int, .{sliceToValue(i64, column_data)}), 278 | .Binary => try writer.print(format_options.binary, .{column_data[0]}), 279 | .VarBinary => { 280 | for (column_data) |c| { 281 | try writer.print(format_options.var_binary, .{c}); 282 | } 283 | }, 284 | .LongVarBinary => { 285 | for (column_data) |c| { 286 | try writer.print(format_options.long_var_binary, .{c}); 287 | } 288 | }, 289 | .Date => try writer.print(format_options.date, .{sliceToValue(CType.SqlDate, column_data)}), 290 | .Time => try writer.print(format_options.time, .{sliceToValue(CType.SqlTime, column_data)}), 291 | .Timestamp => try writer.print(format_options.timestamp, .{sliceToValue(CType.SqlTimestamp, column_data)}), 292 | .TimestampLtz => try writer.print(format_options.timestamp_ltz, .{sliceToValue(CType.SqlTimestamp, column_data)}), 293 | .IntervalMonth => try writer.print(format_options.interval_month, .{sliceToValue(CType.Interval, column_data)}), 294 | .IntervalYear => try writer.print(format_options.interval_year, .{sliceToValue(CType.Interval, column_data)}), 295 | .IntervalYearToMonth => try writer.print(format_options.interval_year_to_month, .{sliceToValue(CType.Interval, column_data)}), 296 | .IntervalDay => try writer.print(format_options.interval_day, .{sliceToValue(CType.Interval, column_data)}), 297 | .IntervalHour => try writer.print(format_options.interval_hour, .{sliceToValue(CType.Interval, column_data)}), 298 | .IntervalMinute => try writer.print(format_options.interval_minute, .{sliceToValue(CType.Interval, column_data)}), 299 | .IntervalSecond => try writer.print(format_options.interval_second, .{sliceToValue(CType.Interval, column_data)}), 300 | .IntervalDayToHour => try writer.print(format_options.interval_day_to_hour, .{sliceToValue(CType.Interval, column_data)}), 301 | .IntervalDayToMinute => try writer.print(format_options.interval_day_to_minute, .{sliceToValue(CType.Interval, column_data)}), 302 | .IntervalDayToSecond => try writer.print(format_options.interval_day_to_second, .{sliceToValue(CType.Interval, column_data)}), 303 | .IntervalHourToMinute => try writer.print(format_options.interval_hour_to_minute, .{sliceToValue(CType.Interval, column_data)}), 304 | .IntervalHourToSecond => try writer.print(format_options.interval_hour_to_second, .{sliceToValue(CType.Interval, column_data)}), 305 | .IntervalMinuteToSecond => try writer.print(format_options.interval_minute_to_second, .{sliceToValue(CType.Interval, column_data)}), 306 | .Guid => try writer.print(format_options.guid, .{sliceToValue(CType.SqlGuid, column_data)}), 307 | } 308 | } 309 | }; 310 | 311 | fn ItemIterator(comptime ItemType: type) type { 312 | return struct { 313 | pub const Self = @This(); 314 | pub const RowType = FetchResult(ItemType); 315 | 316 | rows: []RowType, 317 | row_status: []RowStatus, 318 | 319 | rows_fetched: usize = 0, 320 | current_row: usize = 0, 321 | 322 | is_first: bool = true, 323 | 324 | allocator: Allocator, 325 | statement: odbc.Statement, 326 | 327 | /// Initialze the ResultSet with the given `row_count`. `row_count` will control how many results 328 | /// are fetched every time `statement.fetch()` is called. 329 | pub fn init(allocator: Allocator, statement: odbc.Statement, row_count: usize) !Self { 330 | var result: Self = .{ 331 | .allocator = allocator, 332 | .statement = statement, 333 | .rows = try allocator.alloc(RowType, row_count), 334 | .row_status = try allocator.alloc(RowStatus, row_count), 335 | }; 336 | errdefer result.deinit(); 337 | 338 | try result.statement.setAttribute(.{ .RowBindType = @sizeOf(RowType) }); 339 | try result.statement.setAttribute(.{ .RowArraySize = row_count }); 340 | try result.statement.setAttribute(.{ .RowStatusPointer = result.row_status }); 341 | 342 | try result.bindColumns(); 343 | 344 | return result; 345 | } 346 | 347 | pub fn deinit(self: *Self) void { 348 | self.allocator.free(self.rows); 349 | self.allocator.free(self.row_status); 350 | } 351 | 352 | /// Keep fetching until all results have been retrieved. 353 | pub fn getAllRows(self: *Self) ![]ItemType { 354 | var results = try std.ArrayList(ItemType).initCapacity(self.allocator, 20); 355 | errdefer results.deinit(); 356 | 357 | while (try self.next()) |item| { 358 | try results.append(item); 359 | } 360 | 361 | return results.toOwnedSlice(); 362 | } 363 | 364 | /// Get the next available row. If all current rows have been read, this will attempt to 365 | /// fetch more results with `statement.fetch()`. If `statement.fetch()` returns `error.NoData`, 366 | /// this will return `null`. 367 | pub fn next(self: *Self) !?ItemType { 368 | if (self.is_first) { 369 | try self.statement.setAttribute(.{ .RowsFetchedPointer = &self.rows_fetched }); 370 | self.is_first = false; 371 | } 372 | 373 | while (true) { 374 | if (self.current_row >= self.rows_fetched) { 375 | const has_data = try self.statement.fetch(); 376 | if (!has_data) return null; 377 | self.current_row = 0; 378 | } 379 | 380 | while (self.current_row < self.rows_fetched and self.current_row < self.rows.len) : (self.current_row += 1) { 381 | switch (self.row_status[self.current_row]) { 382 | .Success, .SuccessWithInfo, .Error => { 383 | const item_row = self.rows[self.current_row]; 384 | self.current_row += 1; 385 | return toTarget(ItemType, self.allocator, item_row) catch |err| switch (err) { 386 | error.InvalidNullValue => continue, 387 | else => return err, 388 | }; 389 | }, 390 | else => {}, 391 | } 392 | } 393 | } 394 | } 395 | 396 | /// Bind each column of the result set to their associated row buffers. 397 | /// After this function is called + `statement.fetch()`, you can retrieve 398 | /// result data from this struct. 399 | fn bindColumns(self: *Self) !void { 400 | @setEvalBranchQuota(1_000_000); 401 | comptime var column_number: u16 = 1; 402 | 403 | inline for (std.meta.fields(RowType)) |field| { 404 | comptime if (std.mem.endsWith(u8, field.name, "_len_or_ind")) continue; 405 | 406 | const c_type = comptime blk: { 407 | if (odbc.Types.CType.fromType(field.type)) |c_type| { 408 | break :blk c_type; 409 | } else { 410 | @compileError("CType could not be derived for " ++ @typeName(ItemType) ++ "." ++ field.name ++ " (" ++ @typeName(field.type) ++ ")"); 411 | } 412 | }; 413 | 414 | const FieldType = @typeInfo(field.type); 415 | const FieldDataType = switch (FieldType) { 416 | .Pointer => |info| info.child, 417 | .Array => |info| info.child, 418 | else => field.type, 419 | }; 420 | 421 | const value_ptr: []FieldDataType = switch (FieldType) { 422 | .Pointer => switch (FieldType.Pointer.size) { 423 | .One => @as([*]FieldDataType, @ptrCast(@field(self.rows[0], field.name)))[0..1], 424 | else => @field(self.rows[0], field.name)[0..], 425 | }, 426 | .Array => @field(self.rows[0], field.name)[0..], 427 | else => @as([*]FieldDataType, @ptrCast(&@field(self.rows[0], field.name)))[0..1], 428 | }; 429 | 430 | try self.statement.bindColumn(column_number, c_type, value_ptr, @as([*]i64, @ptrCast(&@field(self.rows[0], field.name ++ "_len_or_ind"))), null); 431 | 432 | column_number += 1; 433 | } 434 | } 435 | }; 436 | } 437 | 438 | /// `RowIterator` is used to fetch query results when you don't have a matching `struct` type to hold each row. 439 | /// This will create a binding for each column in the result set, and then return each row in a `Row` struct 440 | /// one at a time. 441 | const RowIterator = struct { 442 | /// Represents a single column of the result set. Each `Column` instance can hold *multiple rows* of data for 443 | /// that column. The number of rows is limited by the `row_count` parameter passed when initializng `RowIterator`. 444 | const Column = struct { 445 | name: []const u8, 446 | sql_type: odbc.Types.SqlType, 447 | data: []u8, 448 | octet_length: usize, 449 | indicator: []i64, 450 | 451 | fn deinit(column: *Column, allocator: Allocator) void { 452 | allocator.free(column.name); 453 | allocator.free(column.data); 454 | allocator.free(column.indicator); 455 | } 456 | }; 457 | 458 | columns: []Column, 459 | row: Row, 460 | row_status: []RowStatus, 461 | is_first: bool = true, 462 | current_row: usize = 0, 463 | rows_fetched: usize = 0, 464 | 465 | statement: odbc.Statement, 466 | 467 | pub fn init(allocator: Allocator, statement: odbc.Statement, row_count: usize) !RowIterator { 468 | const num_columns = try statement.numResultColumns(); 469 | 470 | var columns = try allocator.alloc(Column, num_columns); 471 | errdefer { 472 | for (columns) |*c| c.deinit(allocator); 473 | allocator.free(columns); 474 | } 475 | 476 | var row_status = try allocator.alloc(RowStatus, row_count); 477 | errdefer allocator.free(row_status); 478 | 479 | try statement.setAttribute(.{ .RowBindType = odbc.sys.SQL_BIND_BY_COLUMN }); 480 | try statement.setAttribute(.{ .RowArraySize = row_count }); 481 | try statement.setAttribute(.{ .RowStatusPointer = row_status }); 482 | 483 | for (columns, 0..) |*column, column_index| { 484 | column.sql_type = (try statement.getColumnAttribute(allocator, column_index + 1, .Type)).Type; 485 | column.name = (try statement.getColumnAttribute(allocator, column_index + 1, .BaseColumnName)).BaseColumnName; 486 | 487 | column.octet_length = if (statement.getColumnAttribute(allocator, column_index + 1, .OctetLength)) |attr| 488 | @as(usize, @intCast(attr.OctetLength)) 489 | else |_| 490 | 0; 491 | 492 | if (column.octet_length == 0) { 493 | const length = (try statement.getColumnAttribute(allocator, column_index + 1, .Length)).Length; 494 | column.octet_length = @as(usize, @intCast(length)); 495 | } 496 | 497 | column.data = try allocator.alloc(u8, row_count * column.octet_length); 498 | column.indicator = try allocator.alloc(i64, row_count); 499 | 500 | try statement.bindColumn(@as(u16, @intCast(column_index + 1)), column.sql_type.defaultCType(), column.data, column.indicator.ptr, column.octet_length); 501 | } 502 | 503 | return RowIterator{ 504 | .statement = statement, 505 | .row_status = row_status, 506 | .columns = columns, 507 | .row = try Row.init(allocator, num_columns), 508 | }; 509 | } 510 | 511 | pub fn deinit(self: *RowIterator, allocator: Allocator) void { 512 | for (self.columns) |*column| column.deinit(allocator); 513 | 514 | allocator.free(self.columns); 515 | allocator.free(self.row_status); 516 | self.row.deinit(allocator); 517 | } 518 | 519 | pub fn next(self: *RowIterator) !?*Row { 520 | if (self.is_first) { 521 | try self.statement.setAttribute(.{ .RowsFetchedPointer = &self.rows_fetched }); 522 | self.is_first = false; 523 | } 524 | 525 | while (true) { 526 | if (self.current_row >= self.rows_fetched) { 527 | const has_data = try self.statement.fetch(); 528 | if (!has_data) return null; 529 | self.current_row = 0; 530 | } 531 | 532 | for (self.row_status[self.current_row..]) |row_status| { 533 | defer self.current_row += 1; 534 | if (self.current_row >= self.rows_fetched) break; 535 | 536 | switch (row_status) { 537 | .Success, .SuccessWithInfo, .Error => { 538 | for (self.row.columns, 0..) |*row_column, column_index| { 539 | const current_column = self.columns[column_index]; 540 | row_column.name = current_column.name; 541 | 542 | const data_start_index = self.current_row * current_column.octet_length; 543 | const data_end_index = data_start_index + current_column.octet_length; 544 | row_column.data = current_column.data[data_start_index..data_end_index]; 545 | row_column.indicator = current_column.indicator[self.current_row]; 546 | row_column.sql_type = current_column.sql_type; 547 | } 548 | 549 | return &self.row; 550 | }, 551 | else => {}, 552 | } 553 | } 554 | } 555 | } 556 | }; 557 | 558 | statement: Statement, 559 | 560 | pub fn init(statement: Statement) ResultSet { 561 | return ResultSet{ 562 | .statement = statement, 563 | }; 564 | } 565 | 566 | pub fn itemIterator(result_set: ResultSet, comptime ItemType: type, allocator: Allocator) !ItemIterator(ItemType) { 567 | return try ItemIterator(ItemType).init(allocator, result_set.statement, 10); 568 | } 569 | 570 | pub fn rowIterator(result_set: ResultSet, allocator: Allocator) !RowIterator { 571 | return try RowIterator.init(allocator, result_set.statement, 10); 572 | } 573 | -------------------------------------------------------------------------------- /src/catalog.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Allocator = std.mem.Allocator; 3 | 4 | const odbc = @import("odbc"); 5 | 6 | /// Used for fetching column metadata from a database. Supports formatted printing by default. 7 | pub const Column = struct { 8 | table_category: ?[]const u8, 9 | table_schema: ?[]const u8, 10 | table_name: []const u8, 11 | column_name: []const u8, 12 | data_type: u16, 13 | type_name: []const u8, 14 | column_size: ?u32, 15 | buffer_length: ?u32, 16 | decimal_digits: ?u16, 17 | num_prec_radix: ?u16, 18 | nullable: odbc.Types.Nullable, 19 | remarks: ?[]const u8, 20 | column_def: ?[]const u8, 21 | sql_data_type: odbc.Types.SqlType, 22 | sql_datetime_sub: ?u16, 23 | char_octet_length: ?u32, 24 | ordinal_position: u32, 25 | is_nullable: ?[]const u8, 26 | 27 | pub fn deinit(self: *Column, allocator: Allocator) void { 28 | if (self.table_category) |tc| allocator.free(tc); 29 | if (self.table_schema) |ts| allocator.free(ts); 30 | allocator.free(self.table_name); 31 | allocator.free(self.column_name); 32 | allocator.free(self.type_name); 33 | if (self.remarks) |r| allocator.free(r); 34 | if (self.column_def) |cd| allocator.free(cd); 35 | if (self.is_nullable) |in| allocator.free(in); 36 | } 37 | 38 | pub fn format(self: @This(), comptime fmt: []const u8, options: std.fmt.FormatOptions, writer: anytype) !void { 39 | _ = fmt; 40 | _ = options; 41 | try writer.print("{s}{{\n", .{@typeName(@TypeOf(self))}); 42 | inline for (std.meta.fields(@TypeOf(self))) |field| { 43 | const field_info = @typeInfo(field.field_type); 44 | const target_type = if (field_info == .Optional) field_info.Optional.child else field.field_type; 45 | 46 | switch (@typeInfo(target_type)) { 47 | .Enum => try writer.print(" {s}: {s}\n", .{ field.name, @tagName(@field(self, field.name)) }), 48 | else => { 49 | const format_string = comptime if (std.meta.trait.isZigString(target_type)) " {s}: {s}\n" else " {s}: {}\n"; 50 | try writer.print(format_string, .{ field.name, @field(self, field.name) }); 51 | }, 52 | } 53 | } 54 | try writer.writeAll("}"); 55 | } 56 | }; 57 | 58 | /// Used for fetching table metadata from a database. Supports formatted printing by default. 59 | pub const Table = struct { 60 | catalog: ?[]const u8, 61 | schema: ?[]const u8, 62 | name: ?[]const u8, 63 | table_type: ?[]const u8, 64 | remarks: ?[]const u8, 65 | 66 | pub fn deinit(self: *Table, allocator: Allocator) void { 67 | if (self.catalog) |cat| allocator.free(cat); 68 | if (self.schema) |schema| allocator.free(schema); 69 | if (self.name) |name| allocator.free(name); 70 | if (self.table_type) |table_type| allocator.free(table_type); 71 | if (self.remarks) |remarks| allocator.free(remarks); 72 | } 73 | 74 | pub fn format(self: @This(), comptime fmt: []const u8, options: std.fmt.FormatOptions, writer: anytype) !void { 75 | _ = fmt; 76 | _ = options; 77 | try writer.print("{s}{{\n", .{@typeName(@TypeOf(self))}); 78 | inline for (std.meta.fields(@TypeOf(self))) |field| { 79 | const field_info = @typeInfo(field.field_type); 80 | const target_type = if (field_info == .Optional) field_info.Optional.child else field.field_type; 81 | 82 | switch (@typeInfo(target_type)) { 83 | .Enum => try writer.print(" {s}: {s}\n", .{ field.name, @tagName(@field(self, field.name)) }), 84 | else => { 85 | const format_string = comptime if (std.meta.trait.isZigString(target_type)) " {s}: {s}\n" else " {s}: {}\n"; 86 | try writer.print(format_string, .{ field.name, @field(self, field.name) }); 87 | }, 88 | } 89 | } 90 | try writer.writeAll("}"); 91 | } 92 | }; 93 | 94 | /// Used for fetching table privilege metadata from databases. Supports formatted printing by default. 95 | pub const TablePrivileges = struct { 96 | category: ?[]const u8, 97 | schema: ?[]const u8, 98 | name: []const u8, 99 | grantor: ?[]const u8, 100 | grantee: []const u8, 101 | privilege: []const u8, 102 | is_grantable: ?[]const u8, 103 | 104 | pub fn deinit(self: *TablePrivileges, allocator: Allocator) void { 105 | if (self.category) |category| allocator.free(category); 106 | if (self.schema) |schema| allocator.free(schema); 107 | if (self.grantor) |grantor| allocator.free(grantor); 108 | if (self.is_grantable) |is_grantable| allocator.free(is_grantable); 109 | allocator.free(self.name); 110 | allocator.free(self.grantee); 111 | allocator.free(self.privilege); 112 | } 113 | 114 | pub fn format(self: @This(), comptime fmt: []const u8, options: std.fmt.FormatOptions, writer: anytype) !void { 115 | _ = fmt; 116 | _ = options; 117 | try writer.print("{s}{{\n", .{@typeName(@TypeOf(self))}); 118 | inline for (std.meta.fields(@TypeOf(self))) |field| { 119 | const field_info = @typeInfo(field.field_type); 120 | const target_type = if (field_info == .Optional) field_info.Optional.child else field.field_type; 121 | 122 | switch (@typeInfo(target_type)) { 123 | .Enum => try writer.print(" {s}: {s}\n", .{ field.name, @tagName(@field(self, field.name)) }), 124 | else => { 125 | const format_string = comptime if (std.meta.trait.isZigString(target_type)) " {s}: {s}\n" else " {s}: {}\n"; 126 | try writer.print(format_string, .{ field.name, @field(self, field.name) }); 127 | }, 128 | } 129 | } 130 | try writer.writeAll("}"); 131 | } 132 | }; 133 | -------------------------------------------------------------------------------- /src/util.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | /// Convert `comptime_int` and `comptime_float` to `i64` and `f64`, respectively. 4 | /// For any other type, this is a no-op. 5 | pub fn EraseComptime(comptime T: type) type { 6 | return switch (T) { 7 | comptime_int => i64, 8 | comptime_float => f64, 9 | else => T, 10 | }; 11 | } 12 | 13 | /// Helper function to convert a slice of bytes to a value of type `T`. 14 | /// Internally calls `std.mem.bytesToValue`. 15 | pub inline fn sliceToValue(comptime T: type, slice: []u8) T { 16 | switch (@typeInfo(T)) { 17 | .Int => |info| { 18 | if (slice.len == 1) { 19 | return @as(T, @intCast(slice[0])); 20 | } else if (slice.len < 4) { 21 | if (info.signedness == .unsigned) { 22 | const int = std.mem.bytesToValue(u16, slice[0..2]); 23 | return @as(T, @intCast(int)); 24 | } else { 25 | const int = std.mem.bytesToValue(i16, slice[0..2]); 26 | return @as(T, @intCast(int)); 27 | } 28 | } else if (slice.len < 8) { 29 | if (info.signedness == .unsigned) { 30 | const int = std.mem.bytesToValue(u32, slice[0..4]); 31 | return @as(T, @intCast(int)); 32 | } else { 33 | const int = std.mem.bytesToValue(i32, slice[0..4]); 34 | return @as(T, @intCast(int)); 35 | } 36 | } else { 37 | if (info.signedness == .unsigned) { 38 | const int = std.mem.bytesToValue(u64, slice[0..8]); 39 | return @as(T, @intCast(int)); 40 | } else { 41 | const int = std.mem.bytesToValue(i64, slice[0..8]); 42 | return @as(T, @intCast(int)); 43 | } 44 | } 45 | }, 46 | .Float => { 47 | if (slice.len >= 4 and slice.len < 8) { 48 | const float = std.mem.bytesToValue(f32, slice[0..4]); 49 | return @as(T, @floatCast(float)); 50 | } else { 51 | const float = std.mem.bytesToValue(f64, slice[0..8]); 52 | return @as(T, @floatCast(float)); 53 | } 54 | }, 55 | .Struct => { 56 | var struct_bytes = [_]u8{0} ** @sizeOf(T); 57 | const slice_end_index = if (@sizeOf(T) >= slice.len) slice.len else @sizeOf(T); 58 | std.mem.copy(u8, struct_bytes[0..], slice[0..slice_end_index]); 59 | return std.mem.bytesToValue(T, &struct_bytes); 60 | }, 61 | else => { 62 | std.debug.assert(slice.len >= @sizeOf(T)); 63 | const ptr = @as(*const [@sizeOf(T)]u8, @ptrCast(slice[0..@sizeOf(T)])); 64 | return std.mem.bytesToValue(T, ptr); 65 | }, 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/zdb.zig: -------------------------------------------------------------------------------- 1 | pub const Connection = @import("Connection.zig"); 2 | pub const ParameterBucket = @import("ParameterBucket.zig"); 3 | pub const ResultSet = @import("ResultSet.zig"); 4 | pub const Cursor = @import("Cursor.zig"); 5 | pub const util = @import("util.zig"); 6 | 7 | pub const odbc = @import("odbc"); 8 | --------------------------------------------------------------------------------