From 3669aa4363d8b375d3764cb1bae838940155ac6e Mon Sep 17 00:00:00 2001 From: Tom van Dinther <39470469+tvandinther@users.noreply.github.com> Date: Sat, 31 Aug 2024 16:30:06 +0200 Subject: [PATCH 01/16] Differentiate Query and Execute query types --- Libsql.Client.Tests/EmbeddedReplicaTests.cs | 2 +- Libsql.Client.Tests/ExecuteTests.cs | 54 +++++++++++++++++++ .../PositionalArgumentTests.cs | 16 +++--- Libsql.Client.Tests/RemoteTests.cs | 2 +- Libsql.Client.Tests/ResultSetTests.cs | 20 +++---- Libsql.Client.Tests/RowsTests.cs | 6 +-- Libsql.Client.Tests/SelectTests.cs | 36 ++++++------- Libsql.Client/DatabaseWrapper.cs | 39 ++++++++++++-- Libsql.Client/IDatabaseClient.cs | 19 ++++++- 9 files changed, 147 insertions(+), 47 deletions(-) create mode 100644 Libsql.Client.Tests/ExecuteTests.cs diff --git a/Libsql.Client.Tests/EmbeddedReplicaTests.cs b/Libsql.Client.Tests/EmbeddedReplicaTests.cs index f8bf8c8..4f4dbff 100644 --- a/Libsql.Client.Tests/EmbeddedReplicaTests.cs +++ b/Libsql.Client.Tests/EmbeddedReplicaTests.cs @@ -25,7 +25,7 @@ public async Task CanConnectAndQueryReplicaDatabase() { await DatabaseClient.Sync(); - var rs = await DatabaseClient.Execute("SELECT COUNT(*) FROM albums"); + var rs = await DatabaseClient.Query("SELECT COUNT(*) FROM albums"); var count = rs.Rows.First().First(); var value = Assert.IsType(count); diff --git a/Libsql.Client.Tests/ExecuteTests.cs b/Libsql.Client.Tests/ExecuteTests.cs new file mode 100644 index 0000000..0c78d1a --- /dev/null +++ b/Libsql.Client.Tests/ExecuteTests.cs @@ -0,0 +1,54 @@ +namespace Libsql.Client.Tests; + +public class ExecuteTests +{ + private readonly IDatabaseClient _db = DatabaseClient.Create().Result; + + [Fact] + public async Task CreateTable_NoRowsAffected() + { + var rowsAffected = await _db.Execute("CREATE TABLE `test` (`id` INTEGER PRIMARY KEY AUTOINCREMENT)"); + Console.WriteLine(rowsAffected); + + Assert.Equal(0ul, rowsAffected); + } + + + + [Fact] + public async Task RowsAffected_ReturnsOne_WhenRepeatedInserts() + { + await _db.Query("CREATE TABLE `test` (`id` INTEGER PRIMARY KEY AUTOINCREMENT)"); + + for (int i = 0; i < 10; i++) + { + var rowsAffected = await _db.Execute("INSERT INTO `test` DEFAULT VALUES"); + Assert.Equal(1ul, rowsAffected); + } + } + + [Fact] + public async Task RowsAffected_ReturnsExpectedValue() + { + await _db.Query("CREATE TABLE `test` (`id` INTEGER PRIMARY KEY AUTOINCREMENT)"); + + var rs = await _db.Query("INSERT INTO `test` DEFAULT VALUES"); + + Assert.Equal(1ul, rs.RowsAffected); + } + + [Fact] + public async Task RowsAffected_ReturnsExectedValue_WhenMultipleUpdates() + { + await _db.Query("CREATE TABLE `test` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `value` INTEGER)"); + + for (int i = 0; i < 10; i++) + { + var rs = await _db.Execute("INSERT INTO `test` DEFAULT VALUES"); + } + + var rs2 = await _db.Query("UPDATE `test` SET `value` = 1"); + + Assert.Equal(10ul, rs2.RowsAffected); + } +} diff --git a/Libsql.Client.Tests/PositionalArgumentTests.cs b/Libsql.Client.Tests/PositionalArgumentTests.cs index a2fe5f1..d63ac0f 100644 --- a/Libsql.Client.Tests/PositionalArgumentTests.cs +++ b/Libsql.Client.Tests/PositionalArgumentTests.cs @@ -7,7 +7,7 @@ public class PositionalArgumentTests [Fact] public async Task SingleParameter() { - var rs = await _db.Execute("SELECT ?", 1); + var rs = await _db.Query("SELECT ?", 1); var row = rs.Rows.First(); var value = row.First(); var integer = Assert.IsType(value); @@ -18,7 +18,7 @@ public async Task SingleParameter() [Fact] public async Task MultipleParameters() { - var rs = await _db.Execute("SELECT ?, ?, ?", 1.0, "2", 3); + var rs = await _db.Query("SELECT ?, ?, ?", 1.0, "2", 3); var row = rs.Rows.First(); var integer = Assert.IsType(row.Skip(2).First()); @@ -28,7 +28,7 @@ public async Task MultipleParameters() [Fact] public async Task BindIntParameter() { - var rs = await _db.Execute("SELECT ?", 1); + var rs = await _db.Query("SELECT ?", 1); var row = rs.Rows.First(); var value = row.First(); var integer = Assert.IsType(value); @@ -39,7 +39,7 @@ public async Task BindIntParameter() [Fact] public async Task BindRealParameter() { - var rs = await _db.Execute("SELECT ?", 1.0); + var rs = await _db.Query("SELECT ?", 1.0); var row = rs.Rows.First(); var value = row.First(); var real = Assert.IsType(value); @@ -50,7 +50,7 @@ public async Task BindRealParameter() [Fact] public async Task BindStringParameter() { - var rs = await _db.Execute("SELECT ?", "hello"); + var rs = await _db.Query("SELECT ?", "hello"); var row = rs.Rows.First(); var value = row.First(); var text = Assert.IsType(value); @@ -61,7 +61,7 @@ public async Task BindStringParameter() [Fact] public async Task BindSingleNullParameter() { - var rs = await _db.Execute("SELECT ?", null); + var rs = await _db.Query("SELECT ?", null); var row = rs.Rows.First(); var value = row.First(); Assert.IsType(value); @@ -70,7 +70,7 @@ public async Task BindSingleNullParameter() [Fact] public async Task BindMultipleParametersWithANull() { - var rs = await _db.Execute("SELECT ?, ?, ?", 1, null, 3); + var rs = await _db.Query("SELECT ?, ?, ?", 1, null, 3); var row = rs.Rows.First(); var value = row.Skip(1).First(); Assert.IsType(value); @@ -79,7 +79,7 @@ public async Task BindMultipleParametersWithANull() [Fact] public async Task BindBlobParameter() { - var rs = await _db.Execute("SELECT ?", new byte[] { 1, 2, 3 }); + var rs = await _db.Query("SELECT ?", new byte[] { 1, 2, 3 }); var row = rs.Rows.First(); var value = row.First(); var blob = Assert.IsType(value); diff --git a/Libsql.Client.Tests/RemoteTests.cs b/Libsql.Client.Tests/RemoteTests.cs index f11ec99..1d5d919 100644 --- a/Libsql.Client.Tests/RemoteTests.cs +++ b/Libsql.Client.Tests/RemoteTests.cs @@ -22,7 +22,7 @@ public RemoteTests(DatabaseContainer fixture) [SkippableFact] public async Task CanConnectAndQueryRemoteDatabase() { - var rs = await DatabaseClient.Execute("SELECT COUNT(*) FROM tracks"); + var rs = await DatabaseClient.Query("SELECT COUNT(*) FROM tracks"); var count = rs.Rows.First().First(); var value = Assert.IsType(count); diff --git a/Libsql.Client.Tests/ResultSetTests.cs b/Libsql.Client.Tests/ResultSetTests.cs index 5ee4200..84659de 100644 --- a/Libsql.Client.Tests/ResultSetTests.cs +++ b/Libsql.Client.Tests/ResultSetTests.cs @@ -7,7 +7,7 @@ public class ResultSetTests [Fact] public async Task Columns_EmptyEnumerable_WhenNonQuery() { - var rs = await _db.Execute("CREATE TABLE `test` (`id` INTEGER PRIMARY KEY AUTOINCREMENT)"); + var rs = await _db.Query("CREATE TABLE `test` (`id` INTEGER PRIMARY KEY AUTOINCREMENT)"); var columns = rs.Columns; Assert.Empty(columns); @@ -16,7 +16,7 @@ public async Task Columns_EmptyEnumerable_WhenNonQuery() [Fact] public async Task Columns_HasLength_WhenNotEmpty() { - var rs = await _db.Execute("SELECT 1, 2, 3"); + var rs = await _db.Query("SELECT 1, 2, 3"); var columns = rs.Columns; Assert.Equal(3, columns.Count()); @@ -25,7 +25,7 @@ public async Task Columns_HasLength_WhenNotEmpty() [Fact] public async Task Columns_NamesAreMarshalled() { - var rs = await _db.Execute("SELECT 1 AS [column_one]"); + var rs = await _db.Query("SELECT 1 AS [column_one]"); var columns = rs.Columns; Assert.Single(columns); @@ -35,7 +35,7 @@ public async Task Columns_NamesAreMarshalled() [Fact] public async Task Columns_NamesAreMarshalled_WhenSeveral() { - var rs = await _db.Execute("SELECT 1 AS [column_one], 2 AS [column_two]"); + var rs = await _db.Query("SELECT 1 AS [column_one], 2 AS [column_two]"); var columns = rs.Columns; Assert.Equal(2, columns.Count()); @@ -48,7 +48,7 @@ public async Task LastInsertRowId_ReturnsExpectedValue() { await _db.Execute("CREATE TABLE `test` (`id` INTEGER PRIMARY KEY AUTOINCREMENT)"); - var rs = await _db.Execute("INSERT INTO `test` DEFAULT VALUES"); + var rs = await _db.Query("INSERT INTO `test` DEFAULT VALUES"); Assert.Equal(1, rs.LastInsertRowId); } @@ -61,23 +61,23 @@ public async Task LastInsertRowId_ReturnsExpectedValue_WhenMultipleInserts() IResultSet rs; for (int i = 0; i < 10; i++) { - rs = await _db.Execute("INSERT INTO `test` DEFAULT VALUES"); + rs = await _db.Query("INSERT INTO `test` DEFAULT VALUES"); Assert.Equal(i + 1, rs.LastInsertRowId); } } [Fact] - public async Task Changes_ReturnsExpectedValue() + public async Task RowsAffected_ReturnsExpectedValue() { await _db.Execute("CREATE TABLE `test` (`id` INTEGER PRIMARY KEY AUTOINCREMENT)"); - var rs = await _db.Execute("INSERT INTO `test` DEFAULT VALUES"); + var rs = await _db.Query("INSERT INTO `test` DEFAULT VALUES"); Assert.Equal(1ul, rs.RowsAffected); } [Fact] - public async Task Changes_ReturnsExectedValue_WhenMultipleUpdates() + public async Task RowsAffected_ReturnsExectedValue_WhenMultipleUpdates() { await _db.Execute("CREATE TABLE `test` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `value` INTEGER)"); @@ -86,7 +86,7 @@ public async Task Changes_ReturnsExectedValue_WhenMultipleUpdates() var rs = await _db.Execute("INSERT INTO `test` DEFAULT VALUES"); } - var rs2 = await _db.Execute("UPDATE `test` SET `value` = 1"); + var rs2 = await _db.Query("UPDATE `test` SET `value` = 1"); Assert.Equal(10ul, rs2.RowsAffected); } diff --git a/Libsql.Client.Tests/RowsTests.cs b/Libsql.Client.Tests/RowsTests.cs index 021bdc7..bfa47de 100644 --- a/Libsql.Client.Tests/RowsTests.cs +++ b/Libsql.Client.Tests/RowsTests.cs @@ -7,7 +7,7 @@ public class RowsTests [Fact] public async Task Rows_WhenEmpty() { - var rs = await _db.Execute("SELECT 1 WHERE 1 = 0"); + var rs = await _db.Query("SELECT 1 WHERE 1 = 0"); var rows = rs.Rows; Assert.Empty(rows); @@ -16,7 +16,7 @@ public async Task Rows_WhenEmpty() [Fact] public async Task Rows_CanIterateTwice() { - var rs = await _db.Execute("SELECT 1"); + var rs = await _db.Query("SELECT 1"); var firstArray = rs.Rows.ToArray(); var secondArray = rs.Rows.ToArray(); @@ -27,7 +27,7 @@ public async Task Rows_CanIterateTwice() [Fact] public async Task Rows_CanPartiallyIterateTwice() { - var rs = await _db.Execute("SELECT 1 UNION SELECT 2 UNION SELECT 3"); + var rs = await _db.Query("SELECT 1 UNION SELECT 2 UNION SELECT 3"); var firstArray = rs.Rows.Take(2).ToArray(); var secondArray = rs.Rows.ToArray(); diff --git a/Libsql.Client.Tests/SelectTests.cs b/Libsql.Client.Tests/SelectTests.cs index d634d4c..ef72c9d 100644 --- a/Libsql.Client.Tests/SelectTests.cs +++ b/Libsql.Client.Tests/SelectTests.cs @@ -7,7 +7,7 @@ public class SelectTests [Fact] public async Task SelectIntType() { - var rs = await _db.Execute("SELECT 1"); + var rs = await _db.Query("SELECT 1"); var row = rs.Rows.First(); var value = row.First(); var integer = Assert.IsType(value); @@ -18,10 +18,10 @@ public async Task SelectIntType() [Fact] public async Task SelectIntType_WhenNull() { - await _db.Execute("CREATE TABLE IF NOT EXISTS test (id INTEGER, value INTEGER)"); - await _db.Execute("INSERT INTO test (value) VALUES (NULL)"); + await _db.Query("CREATE TABLE IF NOT EXISTS test (id INTEGER, value INTEGER)"); + await _db.Query("INSERT INTO test (value) VALUES (NULL)"); - var rs = await _db.Execute("SELECT value FROM test"); + var rs = await _db.Query("SELECT value FROM test"); var row = rs.Rows.First(); var value = row.First(); @@ -31,7 +31,7 @@ public async Task SelectIntType_WhenNull() [Fact] public async Task SelectTextType() { - var rs = await _db.Execute("SELECT 'Hello, World!'"); + var rs = await _db.Query("SELECT 'Hello, World!'"); var row = rs.Rows.First(); var value = row.First(); var text = Assert.IsType(value); @@ -42,7 +42,7 @@ public async Task SelectTextType() [Fact] public async Task SelectTextType_Unicode() { - var rs = await _db.Execute("SELECT 'Привет, мир!'"); + var rs = await _db.Query("SELECT 'Привет, мир!'"); var row = rs.Rows.First(); var value = row.First(); var text = Assert.IsType(value); @@ -53,10 +53,10 @@ public async Task SelectTextType_Unicode() [Fact] public async Task SelectTextType_WhenNull() { - await _db.Execute("CREATE TABLE IF NOT EXISTS test (id INTEGER, value TEXT)"); - await _db.Execute("INSERT INTO test (value) VALUES (NULL)"); + await _db.Query("CREATE TABLE IF NOT EXISTS test (id INTEGER, value TEXT)"); + await _db.Query("INSERT INTO test (value) VALUES (NULL)"); - var rs = await _db.Execute("SELECT value FROM test"); + var rs = await _db.Query("SELECT value FROM test"); var row = rs.Rows.First(); var value = row.First(); @@ -66,7 +66,7 @@ public async Task SelectTextType_WhenNull() [Fact] public async Task SelectRealType() { - var rs = await _db.Execute("SELECT 0.177"); + var rs = await _db.Query("SELECT 0.177"); var row = rs.Rows.First(); var value = row.First(); var real = Assert.IsType(value); @@ -76,10 +76,10 @@ public async Task SelectRealType() [Fact] public async Task SelectRealType_WhenNull() { - await _db.Execute("CREATE TABLE IF NOT EXISTS test (id INTEGER, value REAL)"); - await _db.Execute("INSERT INTO test (value) VALUES (NULL)"); + await _db.Query("CREATE TABLE IF NOT EXISTS test (id INTEGER, value REAL)"); + await _db.Query("INSERT INTO test (value) VALUES (NULL)"); - var rs = await _db.Execute("SELECT value FROM test"); + var rs = await _db.Query("SELECT value FROM test"); var row = rs.Rows.First(); var value = row.First(); @@ -90,7 +90,7 @@ public async Task SelectRealType_WhenNull() public async Task SelectBlobType() { var expected = new byte[]{30, 9, 42, 76}; - var rs = await _db.Execute("SELECT X'1e092a4c'"); + var rs = await _db.Query("SELECT X'1e092a4c'"); var row = rs.Rows.First(); var value = row.First(); var blob = Assert.IsType(value); @@ -100,10 +100,10 @@ public async Task SelectBlobType() [Fact] public async Task SelectBlobType_WhenNull() { - await _db.Execute("CREATE TABLE IF NOT EXISTS test (id INTEGER, value BLOB)"); - await _db.Execute("INSERT INTO test (value) VALUES (NULL)"); + await _db.Query("CREATE TABLE IF NOT EXISTS test (id INTEGER, value BLOB)"); + await _db.Query("INSERT INTO test (value) VALUES (NULL)"); - var rs = await _db.Execute("SELECT value FROM test"); + var rs = await _db.Query("SELECT value FROM test"); var row = rs.Rows.First(); var value = row.First(); @@ -113,7 +113,7 @@ public async Task SelectBlobType_WhenNull() [Fact] public async Task SelectNullType() { - var rs = await _db.Execute("SELECT NULL"); + var rs = await _db.Query("SELECT NULL"); var row = rs.Rows.First(); var value = row.First(); Assert.IsType(value); diff --git a/Libsql.Client/DatabaseWrapper.cs b/Libsql.Client/DatabaseWrapper.cs index d8d6cc4..d2be698 100644 --- a/Libsql.Client/DatabaseWrapper.cs +++ b/Libsql.Client/DatabaseWrapper.cs @@ -136,8 +136,27 @@ internal unsafe void Connect() error.ThrowIfNonZero(exitCode, "Failed to connect to database"); } + + public async Task Query(string sql) + { + return await Task.Run(() => + { + var statement = new Statement(_connection, sql); + return QueryStatement(statement); + }); + } + + public async Task Query(string sql, params object[] args) + { + return await Task.Run(() => { + var statement = new Statement(_connection, sql); + statement.Bind(args); + + return QueryStatement(statement); + }); + } - public async Task Execute(string sql) + public async Task Execute(string sql) { return await Task.Run(() => { @@ -146,7 +165,7 @@ public async Task Execute(string sql) }); } - public async Task Execute(string sql, params object[] args) + public async Task Execute(string sql, params object[] args) { return await Task.Run(() => { var statement = new Statement(_connection, sql); @@ -156,8 +175,7 @@ public async Task Execute(string sql, params object[] args) }); } - // TODO: Differentiate query statements and execute statements. - private unsafe IResultSet ExecuteStatement(Statement statement) + private unsafe IResultSet QueryStatement(Statement statement) { var error = new Error(); var rows = new libsql_rows_t(); @@ -176,6 +194,19 @@ private unsafe IResultSet ExecuteStatement(Statement statement) ); } + private unsafe ulong ExecuteStatement(Statement statement) + { + var error = new Error(); + int exitCode; + + exitCode = Bindings.libsql_execute_stmt(statement.Stmt, &error.Ptr); + statement.Dispose(); + + error.ThrowIfNonZero(exitCode, "Failed to execute statement"); + + return Bindings.libsql_changes(_connection); + } + public async Task Sync() { if (_type != DatabaseType.EmbeddedReplica) diff --git a/Libsql.Client/IDatabaseClient.cs b/Libsql.Client/IDatabaseClient.cs index f84d023..c7747ed 100644 --- a/Libsql.Client/IDatabaseClient.cs +++ b/Libsql.Client/IDatabaseClient.cs @@ -13,7 +13,7 @@ public interface IDatabaseClient /// The SQL query to execute. /// The result set returned by the query. /// Thrown when the query fails to execute. - Task Execute(string sql); + Task Query(string sql); /// /// Executes the given SQL query with the specified parameters and returns the result set. @@ -22,7 +22,22 @@ public interface IDatabaseClient /// The parameters to use in the query. /// The result set returned by the query. /// Thrown when the query fails to execute. - Task Execute(string sql, params object[] args); + Task Query(string sql, params object[] args); + + /// + /// Executes the given SQL. + /// + /// The SQL query to execute. + /// Thrown when the SQL fails to execute. + Task Execute(string sql); + + /// + /// Executes the given SQL with the specified parameters. + /// + /// The SQL query to execute. + /// The parameters to use in the query. + /// Thrown when the SQL fails to execute. + Task Execute(string sql, params object[] args); /// /// Synchronises the embedded replica database with the remote database. From 5808b8719d86b1730a315dbbbf1bcecc3780bb45 Mon Sep 17 00:00:00 2001 From: Tom van Dinther <39470469+tvandinther@users.noreply.github.com> Date: Sat, 31 Aug 2024 17:49:00 +0200 Subject: [PATCH 02/16] Refactor to add statement interface --- Libsql.Client.Tests/StatementTests.cs | 35 +++++ Libsql.Client/DatabaseWrapper.cs | 66 +++++++--- Libsql.Client/IDatabaseClient.cs | 25 ++++ Libsql.Client/IStatement.cs | 20 +++ Libsql.Client/Statement.cs | 133 ++++++------------- Libsql.Client/StatementWrapper.cs | 183 ++++++++++++++++++++++++++ Libsql.Client/Values/Integer.cs | 3 + 7 files changed, 353 insertions(+), 112 deletions(-) create mode 100644 Libsql.Client.Tests/StatementTests.cs create mode 100644 Libsql.Client/IStatement.cs create mode 100644 Libsql.Client/StatementWrapper.cs diff --git a/Libsql.Client.Tests/StatementTests.cs b/Libsql.Client.Tests/StatementTests.cs new file mode 100644 index 0000000..7f9caeb --- /dev/null +++ b/Libsql.Client.Tests/StatementTests.cs @@ -0,0 +1,35 @@ +namespace Libsql.Client.Tests; + +public class StatementTests +{ + private readonly IDatabaseClient _db = DatabaseClient.Create().Result; + + public StatementTests() + { + _db.Execute("CREATE TABLE `test` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT)"); + _db.Execute("INSERT INTO `test` VALUES ('a', 'b', 'c')"); + } + + [Fact] + public async Task Prepare_ReturnedStatement_CanBeExecuted() + { + var statement = await _db.Prepare("SELECT `id` FROM `test` WHERE `name` = ?"); + + Assert.NotNull(statement); + Assert.IsAssignableFrom(statement); + } + + [Fact] + public async Task Statement_CanBind_Integer() + { + var statement = await _db.Prepare("SELECT ?"); + + statement.Bind(1); + var rs = await statement.Query(); + var row = rs.Rows.First(); + var value = row.First(); + var integer = Assert.IsType(value); + + Assert.Equal(1, integer); + } +} \ No newline at end of file diff --git a/Libsql.Client/DatabaseWrapper.cs b/Libsql.Client/DatabaseWrapper.cs index d2be698..d757eab 100644 --- a/Libsql.Client/DatabaseWrapper.cs +++ b/Libsql.Client/DatabaseWrapper.cs @@ -137,45 +137,70 @@ internal unsafe void Connect() error.ThrowIfNonZero(exitCode, "Failed to connect to database"); } - public async Task Query(string sql) + public Task Query(string sql) { - return await Task.Run(() => + return Task.Run(() => { - var statement = new Statement(_connection, sql); + var statement = new StatementWrapper(this, _connection, sql); return QueryStatement(statement); }); } - public async Task Query(string sql, params object[] args) + public Task Query(string sql, params object[] args) { - return await Task.Run(() => { - var statement = new Statement(_connection, sql); - statement.Bind(args); + return Task.Run(() => { + var statement = new StatementWrapper(this, _connection, sql); + statement.BindAll(args); return QueryStatement(statement); }); } - public async Task Execute(string sql) + public Task Execute(string sql) { - return await Task.Run(() => + return Task.Run(() => { - var statement = new Statement(_connection, sql); + var statement = new StatementWrapper(this, _connection, sql); return ExecuteStatement(statement); }); } - public async Task Execute(string sql, params object[] args) + public Task Execute(string sql, params object[] args) { - return await Task.Run(() => { - var statement = new Statement(_connection, sql); - statement.Bind(args); + return Task.Run(() => { + var statement = new StatementWrapper(this, _connection, sql); + statement.BindAll(args); return ExecuteStatement(statement); }); } + + public Task Query(IStatement statement) + { + return statement.Query(); + } + + public Task Execute(IStatement statement) + { + return statement.Execute(); + } + + internal Task Query(StatementWrapper statement) + { + return Task.Run(() => { + return QueryStatement(statement); + }); + } + + internal Task Execute(StatementWrapper statement) + { + return Task.Run(() => { + return ExecuteStatement(statement); + }); + } + - private unsafe IResultSet QueryStatement(Statement statement) + private unsafe IResultSet QueryStatement(StatementWrapper statement) { var error = new Error(); var rows = new libsql_rows_t(); @@ -194,7 +219,7 @@ private unsafe IResultSet QueryStatement(Statement statement) ); } - private unsafe ulong ExecuteStatement(Statement statement) + private unsafe ulong ExecuteStatement(StatementWrapper statement) { var error = new Error(); int exitCode; @@ -207,14 +232,14 @@ private unsafe ulong ExecuteStatement(Statement statement) return Bindings.libsql_changes(_connection); } - public async Task Sync() + public Task Sync() { if (_type != DatabaseType.EmbeddedReplica) { throw new InvalidOperationException("Cannot sync a non-replica database"); } - await Task.Run(() => + return Task.Run(() => { unsafe { @@ -228,6 +253,11 @@ await Task.Run(() => }); } + public Task Prepare(string sql) + { + return Task.Run(() => new StatementWrapper(this, _connection, sql)); + } + private void ReleaseUnmanagedResources() { Bindings.libsql_disconnect(_connection); diff --git a/Libsql.Client/IDatabaseClient.cs b/Libsql.Client/IDatabaseClient.cs index c7747ed..26e1a59 100644 --- a/Libsql.Client/IDatabaseClient.cs +++ b/Libsql.Client/IDatabaseClient.cs @@ -24,10 +24,19 @@ public interface IDatabaseClient /// Thrown when the query fails to execute. Task Query(string sql, params object[] args); + /// + /// Executes the given prepared statement and returns the result set. + /// + /// The prepared statement to execute. + /// The result set returned by the prepared statement. + /// Thrown when the prepared statement fails to execute. + Task Query(IStatement statement); + /// /// Executes the given SQL. /// /// The SQL query to execute. + /// The number of affected rows. /// Thrown when the SQL fails to execute. Task Execute(string sql); @@ -36,9 +45,25 @@ public interface IDatabaseClient /// /// The SQL query to execute. /// The parameters to use in the query. + /// The number of affected rows. /// Thrown when the SQL fails to execute. Task Execute(string sql, params object[] args); + /// + /// Executes the given prepared statement. + /// + /// The prepared statement to execute. + /// The number of affected rows. + /// Thrown when the prepared statement fails to execute. + Task Execute(IStatement statement); + + /// + /// Prepares a statement with the database ready for values to be bound to it. + /// + /// The SQL statement to prepare. + /// A statement to bind values to. + Task Prepare(string sql); + /// /// Synchronises the embedded replica database with the remote database. /// diff --git a/Libsql.Client/IStatement.cs b/Libsql.Client/IStatement.cs new file mode 100644 index 0000000..ca23798 --- /dev/null +++ b/Libsql.Client/IStatement.cs @@ -0,0 +1,20 @@ +using System.Threading.Tasks; + +namespace Libsql.Client +{ + /// + /// Represents the result set of a SQL query. + /// + public interface IStatement + { + int ValuesBound { get; } + void Bind(Integer integer); + // void Bind(int integer); + void Bind(Blob blob); + void Bind(Real real); + void Bind(Text text); + void BindNull(); + Task Execute(); + Task Query(); + } +} diff --git a/Libsql.Client/Statement.cs b/Libsql.Client/Statement.cs index a261f89..962e3a8 100644 --- a/Libsql.Client/Statement.cs +++ b/Libsql.Client/Statement.cs @@ -1,94 +1,39 @@ -using System; -using System.Text; - -namespace Libsql.Client -{ - internal class Statement - { - public libsql_stmt_t Stmt; - private libsql_connection_t _connection; - - public unsafe Statement(libsql_connection_t connection, string sql) - { - _connection = connection; - Stmt = new libsql_stmt_t(); - var error = new Error(); - int exitCode; - - fixed (byte* sqlPtr = Encoding.UTF8.GetBytes(sql)) - { - fixed (libsql_stmt_t* statementPtr = &Stmt) - { - exitCode = Bindings.libsql_prepare(_connection, sqlPtr, statementPtr, &error.Ptr); - } - } - - error.ThrowIfNonZero(exitCode, $"Failed to prepare statement for: {sql}"); - } - - public unsafe void Bind(object[] values) - { - var error = new Error(); - int exitCode; - - if (values is null) - { - exitCode = Bindings.libsql_bind_null(Stmt, 1, &error.Ptr); - error.ThrowIfNonZero(exitCode, "Failed to bind null parameter"); - } - else - { - for (var i = 0; i < values.Length; i++) - { - var arg = values[i]; - - - if (arg is int val) { - exitCode = Bindings.libsql_bind_int(Stmt, i + 1, val, &error.Ptr); - } - else if (arg is double d) { - exitCode = Bindings.libsql_bind_float(Stmt, i + 1, d, &error.Ptr); - } - else if (arg is string s) { - fixed (byte* sPtr = Encoding.UTF8.GetBytes(s)) - { - exitCode = Bindings.libsql_bind_string(Stmt, i + 1, sPtr, &error.Ptr); - } - } - else if (arg is byte[] b) { - fixed (byte* bPtr = b) - { - exitCode = Bindings.libsql_bind_blob(Stmt, i + 1, bPtr, b.Length, &error.Ptr); - } - } - else if (arg is null) - { - exitCode = Bindings.libsql_bind_null(Stmt, i + 1, &error.Ptr); - } - else - { - throw new ArgumentException($"Unsupported argument type: {arg.GetType()}"); - } - - error.ThrowIfNonZero(exitCode, $"Failed to bind parameter. Type: {(arg is null ? "null" : arg.GetType().ToString())} Value: {arg}"); - } - } - } - - private void ReleaseUnmanagedResources() - { - Bindings.libsql_free_stmt(Stmt); - } - - public void Dispose() - { - ReleaseUnmanagedResources(); - GC.SuppressFinalize(this); - } - - ~Statement() - { - ReleaseUnmanagedResources(); - } - } -} +// using System.Threading.Tasks; + +// namespace Libsql.Client +// { +// internal class Statement : IStatement +// { +// internal readonly StatementWrapper _stmt; + +// public Statement(StatementWrapper statementWrapper) +// { +// _stmt = statementWrapper; +// } + +// public void Bind(Integer integer) +// { +// _stmt.BindInt(1, integer.Value); +// } + +// public void Bind(Blob blob) +// { +// throw new System.NotImplementedException(); +// } + +// public void Bind(Real real) +// { +// throw new System.NotImplementedException(); +// } + +// public void Bind(Text text) +// { +// throw new System.NotImplementedException(); +// } + +// public void BindNull() +// { +// throw new System.NotImplementedException(); +// } +// } +// } \ No newline at end of file diff --git a/Libsql.Client/StatementWrapper.cs b/Libsql.Client/StatementWrapper.cs new file mode 100644 index 0000000..f150e4a --- /dev/null +++ b/Libsql.Client/StatementWrapper.cs @@ -0,0 +1,183 @@ +using System; +using System.Diagnostics; +using System.Text; +using System.Threading.Tasks; + +namespace Libsql.Client +{ + internal class StatementWrapper : IStatement + { + int IStatement.ValuesBound => _bindIndex - 1; + private int _bindIndex = 1; + public readonly libsql_stmt_t Stmt; + private readonly libsql_connection_t _connection; + private readonly DatabaseWrapper _database; + + public unsafe StatementWrapper(DatabaseWrapper database, libsql_connection_t connection, string sql) + { + _database = database; + _connection = connection; + Stmt = new libsql_stmt_t(); + var error = new Error(); + int exitCode; + + fixed (byte* sqlPtr = Encoding.UTF8.GetBytes(sql)) + { + fixed (libsql_stmt_t* statementPtr = &Stmt) + { + exitCode = Bindings.libsql_prepare(_connection, sqlPtr, statementPtr, &error.Ptr); + } + } + + error.ThrowIfNonZero(exitCode, $"Failed to prepare statement for: {sql}"); + + Debug.Assert(_database != null); + } + + public unsafe void BindAll(object[] values) + { + if (values is null) + { + var error = new Error(); + var exitCode = Bindings.libsql_bind_null(Stmt, 1, &error.Ptr); + error.ThrowIfNonZero(exitCode, "Failed to bind null parameters"); + } + else + { + foreach (var arg in values) + { + switch (arg) + { + case int val: + BindInt(val); + break; + case double d: + BindFloat(d); + break; + case string s: + BindString(s); + break; + case byte[] b: + BindBlob(b); + break; + case null: + Bind(); + break; + default: + throw new ArgumentException($"Unsupported argument type: {arg.GetType()}"); + } + } + } + } + + private unsafe void BindInt(int value) + { + var error = new Error(); + var index = _bindIndex; + Console.WriteLine($"Binding int at index {index}"); + var exitCode = Bindings.libsql_bind_int(Stmt, index, value, &error.Ptr); + + error.ThrowIfNonZero(exitCode, $"Failed to bind integer at index {index} with value {value}"); + _bindIndex++; + } + + private unsafe void BindFloat(double value) + { + var error = new Error(); + var index = _bindIndex; + Console.WriteLine($"Binding int at index {index}"); + var exitCode = Bindings.libsql_bind_float(Stmt, index, value, &error.Ptr); + + error.ThrowIfNonZero(exitCode, $"Failed to bind integer at index {index} with value {value}"); + _bindIndex++; + } + + private unsafe void BindString(string value) + { + var error = new Error(); + var index = _bindIndex; + Console.WriteLine($"Binding int at index {index}"); + fixed (byte* sPtr = Encoding.UTF8.GetBytes(value)) { + var exitCode = Bindings.libsql_bind_string(Stmt, index, sPtr, &error.Ptr); + + error.ThrowIfNonZero(exitCode, $"Failed to bind integer at index {index} with value {value}"); + } + _bindIndex++; + } + + private unsafe void BindBlob(byte[] value) + { + var error = new Error(); + var index = _bindIndex; + Console.WriteLine($"Binding int at index {index}"); + fixed (byte* bPtr = value) { + var exitCode = Bindings.libsql_bind_blob(Stmt, index, bPtr, value.Length, &error.Ptr); + + error.ThrowIfNonZero(exitCode, $"Failed to bind integer at index {index} with value {value}"); + } + _bindIndex++; + } + + private unsafe void Bind() + { + var error = new Error(); + var index = _bindIndex; + Console.WriteLine($"Binding int at index {index}"); + var exitCode = Bindings.libsql_bind_null(Stmt, index, &error.Ptr); + + error.ThrowIfNonZero(exitCode, $"Failed to bind null at index {index}"); + _bindIndex++; + } + + private void ReleaseUnmanagedResources() + { + Bindings.libsql_free_stmt(Stmt); + } + + public void Dispose() + { + ReleaseUnmanagedResources(); + GC.SuppressFinalize(this); + } + + public void Bind(Integer integer) + { + BindInt(integer.Value); + } + + public void Bind(Blob blob) + { + throw new NotImplementedException(); + } + + public void Bind(Real real) + { + throw new NotImplementedException(); + } + + public void Bind(Text text) + { + throw new NotImplementedException(); + } + + public void BindNull() + { + throw new NotImplementedException(); + } + + public Task Execute() + { + return _database.Execute(this); + } + + public Task Query() + { + return _database.Query(this); + } + + ~StatementWrapper() + { + ReleaseUnmanagedResources(); + } + } +} diff --git a/Libsql.Client/Values/Integer.cs b/Libsql.Client/Values/Integer.cs index 3e68dd7..018fd21 100644 --- a/Libsql.Client/Values/Integer.cs +++ b/Libsql.Client/Values/Integer.cs @@ -23,6 +23,9 @@ internal Integer(int value) public static bool operator !=(Integer left, Integer right) => !(left == right); + public static implicit operator Integer(int value) => new Integer(value); + public static implicit operator int(Integer integer) => integer.Value; + public override bool Equals(Value other) { if (other is Integer otherInteger) From 54766623551097ebe046888381d4f354559bdd33 Mon Sep 17 00:00:00 2001 From: Tom van Dinther <39470469+tvandinther@users.noreply.github.com> Date: Sat, 31 Aug 2024 17:55:14 +0200 Subject: [PATCH 03/16] Implement Real binding --- Libsql.Client.Tests/StatementTests.cs | 17 ++++++++++-- Libsql.Client/IStatement.cs | 4 +-- Libsql.Client/Statement.cs | 39 --------------------------- Libsql.Client/StatementWrapper.cs | 8 +++--- Libsql.Client/Values/Integer.cs | 1 + Libsql.Client/Values/Real.cs | 4 +++ 6 files changed, 26 insertions(+), 47 deletions(-) delete mode 100644 Libsql.Client/Statement.cs diff --git a/Libsql.Client.Tests/StatementTests.cs b/Libsql.Client.Tests/StatementTests.cs index 7f9caeb..7ede3e5 100644 --- a/Libsql.Client.Tests/StatementTests.cs +++ b/Libsql.Client.Tests/StatementTests.cs @@ -23,8 +23,7 @@ public async Task Prepare_ReturnedStatement_CanBeExecuted() public async Task Statement_CanBind_Integer() { var statement = await _db.Prepare("SELECT ?"); - - statement.Bind(1); + statement.Bind(new Integer(1)); var rs = await statement.Query(); var row = rs.Rows.First(); var value = row.First(); @@ -32,4 +31,18 @@ public async Task Statement_CanBind_Integer() Assert.Equal(1, integer); } + + [Fact] + public async Task Statement_CanBind_Real() + { + var statement = await _db.Prepare("SELECT ?"); + + statement.Bind(0.5); + var rs = await statement.Query(); + var row = rs.Rows.First(); + var value = row.First(); + var integer = Assert.IsType(value); + + Assert.Equal(0.5, integer); + } } \ No newline at end of file diff --git a/Libsql.Client/IStatement.cs b/Libsql.Client/IStatement.cs index ca23798..6d7f4cf 100644 --- a/Libsql.Client/IStatement.cs +++ b/Libsql.Client/IStatement.cs @@ -9,10 +9,10 @@ public interface IStatement { int ValuesBound { get; } void Bind(Integer integer); - // void Bind(int integer); - void Bind(Blob blob); + void Bind(Real real); void Bind(Text text); + void Bind(Blob blob); void BindNull(); Task Execute(); Task Query(); diff --git a/Libsql.Client/Statement.cs b/Libsql.Client/Statement.cs deleted file mode 100644 index 962e3a8..0000000 --- a/Libsql.Client/Statement.cs +++ /dev/null @@ -1,39 +0,0 @@ -// using System.Threading.Tasks; - -// namespace Libsql.Client -// { -// internal class Statement : IStatement -// { -// internal readonly StatementWrapper _stmt; - -// public Statement(StatementWrapper statementWrapper) -// { -// _stmt = statementWrapper; -// } - -// public void Bind(Integer integer) -// { -// _stmt.BindInt(1, integer.Value); -// } - -// public void Bind(Blob blob) -// { -// throw new System.NotImplementedException(); -// } - -// public void Bind(Real real) -// { -// throw new System.NotImplementedException(); -// } - -// public void Bind(Text text) -// { -// throw new System.NotImplementedException(); -// } - -// public void BindNull() -// { -// throw new System.NotImplementedException(); -// } -// } -// } \ No newline at end of file diff --git a/Libsql.Client/StatementWrapper.cs b/Libsql.Client/StatementWrapper.cs index f150e4a..22eb96a 100644 --- a/Libsql.Client/StatementWrapper.cs +++ b/Libsql.Client/StatementWrapper.cs @@ -145,17 +145,17 @@ public void Bind(Integer integer) BindInt(integer.Value); } - public void Bind(Blob blob) + public void Bind(Real real) { - throw new NotImplementedException(); + BindFloat(real.Value); } - public void Bind(Real real) + public void Bind(Text text) { throw new NotImplementedException(); } - public void Bind(Text text) + public void Bind(Blob blob) { throw new NotImplementedException(); } diff --git a/Libsql.Client/Values/Integer.cs b/Libsql.Client/Values/Integer.cs index 018fd21..b1c0851 100644 --- a/Libsql.Client/Values/Integer.cs +++ b/Libsql.Client/Values/Integer.cs @@ -24,6 +24,7 @@ internal Integer(int value) public static bool operator !=(Integer left, Integer right) => !(left == right); public static implicit operator Integer(int value) => new Integer(value); + public static implicit operator int(Integer integer) => integer.Value; public override bool Equals(Value other) diff --git a/Libsql.Client/Values/Real.cs b/Libsql.Client/Values/Real.cs index 7fa6b01..bfd5cfc 100644 --- a/Libsql.Client/Values/Real.cs +++ b/Libsql.Client/Values/Real.cs @@ -24,6 +24,10 @@ internal Real(double value) public static bool operator !=(Real left, Real right) => !(left == right); + public static implicit operator Real(double value) => new Real(value); + + public static implicit operator double(Real integer) => integer.Value; + public override bool Equals(Value other) { if (other is Real otherReal) From d4d314986dd4149559b2d7010fec5167a03aab0b Mon Sep 17 00:00:00 2001 From: Tom van Dinther <39470469+tvandinther@users.noreply.github.com> Date: Sat, 31 Aug 2024 17:57:44 +0200 Subject: [PATCH 04/16] Implement Text binding --- Libsql.Client.Tests/StatementTests.cs | 14 ++++++++++++++ Libsql.Client/StatementWrapper.cs | 2 +- Libsql.Client/Values/Text.cs | 4 ++++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/Libsql.Client.Tests/StatementTests.cs b/Libsql.Client.Tests/StatementTests.cs index 7ede3e5..ada6ac5 100644 --- a/Libsql.Client.Tests/StatementTests.cs +++ b/Libsql.Client.Tests/StatementTests.cs @@ -45,4 +45,18 @@ public async Task Statement_CanBind_Real() Assert.Equal(0.5, integer); } + + [Fact] + public async Task Statement_CanBind_Text() + { + var statement = await _db.Prepare("SELECT ?"); + + statement.Bind("hello"); + var rs = await statement.Query(); + var row = rs.Rows.First(); + var value = row.First(); + var text = Assert.IsType(value); + + Assert.Equal("hello", text); + } } \ No newline at end of file diff --git a/Libsql.Client/StatementWrapper.cs b/Libsql.Client/StatementWrapper.cs index 22eb96a..b9c2a1b 100644 --- a/Libsql.Client/StatementWrapper.cs +++ b/Libsql.Client/StatementWrapper.cs @@ -152,7 +152,7 @@ public void Bind(Real real) public void Bind(Text text) { - throw new NotImplementedException(); + BindString(text.Value); } public void Bind(Blob blob) diff --git a/Libsql.Client/Values/Text.cs b/Libsql.Client/Values/Text.cs index 866c53d..0995af1 100644 --- a/Libsql.Client/Values/Text.cs +++ b/Libsql.Client/Values/Text.cs @@ -22,6 +22,10 @@ internal Text(string value) public static bool operator ==(Text left, Text right) => left?.Value == right?.Value; public static bool operator !=(Text left, Text right) => !(left == right); + + public static implicit operator Text(string value) => new Text(value); + + public static implicit operator string(Text text) => text.Value; public override bool Equals(Value other) { From 4fd67598db2bb6a4c648ee45a1cdbaa1d5f07f91 Mon Sep 17 00:00:00 2001 From: Tom van Dinther <39470469+tvandinther@users.noreply.github.com> Date: Sat, 31 Aug 2024 18:02:33 +0200 Subject: [PATCH 05/16] Implement Blob binding --- Libsql.Client.Tests/StatementTests.cs | 31 +++++++++++++++++++++------ Libsql.Client/StatementWrapper.cs | 2 +- Libsql.Client/Values/Blob.cs | 4 ++++ 3 files changed, 30 insertions(+), 7 deletions(-) diff --git a/Libsql.Client.Tests/StatementTests.cs b/Libsql.Client.Tests/StatementTests.cs index ada6ac5..46e49d2 100644 --- a/Libsql.Client.Tests/StatementTests.cs +++ b/Libsql.Client.Tests/StatementTests.cs @@ -23,40 +23,59 @@ public async Task Prepare_ReturnedStatement_CanBeExecuted() public async Task Statement_CanBind_Integer() { var statement = await _db.Prepare("SELECT ?"); - statement.Bind(new Integer(1)); + var expected = 1; + + statement.Bind(new Integer(expected)); var rs = await statement.Query(); var row = rs.Rows.First(); var value = row.First(); var integer = Assert.IsType(value); - Assert.Equal(1, integer); + Assert.Equal(expected, integer); } [Fact] public async Task Statement_CanBind_Real() { var statement = await _db.Prepare("SELECT ?"); + var expected = 0.5; - statement.Bind(0.5); + statement.Bind(expected); var rs = await statement.Query(); var row = rs.Rows.First(); var value = row.First(); var integer = Assert.IsType(value); - Assert.Equal(0.5, integer); + Assert.Equal(expected, integer); } [Fact] public async Task Statement_CanBind_Text() { var statement = await _db.Prepare("SELECT ?"); + var expected = "hello"; - statement.Bind("hello"); + statement.Bind(expected); var rs = await statement.Query(); var row = rs.Rows.First(); var value = row.First(); var text = Assert.IsType(value); - Assert.Equal("hello", text); + Assert.Equal(expected, text); + } + + [Fact] + public async Task Statement_CanBind_Blob() + { + var statement = await _db.Prepare("SELECT ?"); + var expected = new byte[]{30, 9, 42, 76}; + + statement.Bind(expected); + var rs = await statement.Query(); + var row = rs.Rows.First(); + var value = row.First(); + var blob = Assert.IsType(value); + + Assert.Equal(expected, blob); } } \ No newline at end of file diff --git a/Libsql.Client/StatementWrapper.cs b/Libsql.Client/StatementWrapper.cs index b9c2a1b..903ccd0 100644 --- a/Libsql.Client/StatementWrapper.cs +++ b/Libsql.Client/StatementWrapper.cs @@ -157,7 +157,7 @@ public void Bind(Text text) public void Bind(Blob blob) { - throw new NotImplementedException(); + BindBlob(blob.Value); } public void BindNull() diff --git a/Libsql.Client/Values/Blob.cs b/Libsql.Client/Values/Blob.cs index 6e1aeed..5c8adc4 100644 --- a/Libsql.Client/Values/Blob.cs +++ b/Libsql.Client/Values/Blob.cs @@ -47,6 +47,10 @@ public override string ToString() public static bool operator !=(Blob left, Blob right) => !(left == right); + public static implicit operator Blob(byte[] value) => new Blob(value); + + public static implicit operator byte[](Blob blob) => blob.Value; + public override bool Equals(Value other) { if (other is Blob otherBlob) From 58f32d1406edc241dc66007cf677bef2f5be864a Mon Sep 17 00:00:00 2001 From: Tom van Dinther <39470469+tvandinther@users.noreply.github.com> Date: Sat, 31 Aug 2024 18:04:20 +0200 Subject: [PATCH 06/16] Implement Null binding --- Libsql.Client.Tests/StatementTests.cs | 13 +++++++++++++ Libsql.Client/StatementWrapper.cs | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/Libsql.Client.Tests/StatementTests.cs b/Libsql.Client.Tests/StatementTests.cs index 46e49d2..554c9fd 100644 --- a/Libsql.Client.Tests/StatementTests.cs +++ b/Libsql.Client.Tests/StatementTests.cs @@ -78,4 +78,17 @@ public async Task Statement_CanBind_Blob() Assert.Equal(expected, blob); } + + [Fact] + public async Task Statement_CanBind_Null() + { + var statement = await _db.Prepare("SELECT ?"); + + statement.BindNull(); + var rs = await statement.Query(); + var row = rs.Rows.First(); + var value = row.First(); + + Assert.IsType(value); + } } \ No newline at end of file diff --git a/Libsql.Client/StatementWrapper.cs b/Libsql.Client/StatementWrapper.cs index 903ccd0..a335c44 100644 --- a/Libsql.Client/StatementWrapper.cs +++ b/Libsql.Client/StatementWrapper.cs @@ -162,7 +162,7 @@ public void Bind(Blob blob) public void BindNull() { - throw new NotImplementedException(); + Bind(); } public Task Execute() From 83b16b7822180486c19f9873a00b706313996faa Mon Sep 17 00:00:00 2001 From: Tom van Dinther <39470469+tvandinther@users.noreply.github.com> Date: Sat, 31 Aug 2024 18:18:58 +0200 Subject: [PATCH 07/16] Update README.md --- Libsql.Client/IStatement.cs | 2 +- Libsql.Client/StatementWrapper.cs | 2 +- README.md | 45 ++++++++++++++++++++++++++++--- 3 files changed, 43 insertions(+), 6 deletions(-) diff --git a/Libsql.Client/IStatement.cs b/Libsql.Client/IStatement.cs index 6d7f4cf..144dbf8 100644 --- a/Libsql.Client/IStatement.cs +++ b/Libsql.Client/IStatement.cs @@ -7,7 +7,7 @@ namespace Libsql.Client /// public interface IStatement { - int ValuesBound { get; } + int BoundValuesCount { get; } void Bind(Integer integer); void Bind(Real real); diff --git a/Libsql.Client/StatementWrapper.cs b/Libsql.Client/StatementWrapper.cs index a335c44..713fe66 100644 --- a/Libsql.Client/StatementWrapper.cs +++ b/Libsql.Client/StatementWrapper.cs @@ -7,7 +7,7 @@ namespace Libsql.Client { internal class StatementWrapper : IStatement { - int IStatement.ValuesBound => _bindIndex - 1; + int IStatement.BoundValuesCount => _bindIndex - 1; private int _bindIndex = 1; public readonly libsql_stmt_t Stmt; private readonly libsql_connection_t _connection; diff --git a/README.md b/README.md index e973069..19f8e69 100644 --- a/README.md +++ b/README.md @@ -14,12 +14,13 @@ A .NET client library for libsql. - From connection string. - Executing SQL statements: - Non-parameterised. + - With positional parameters +- Prepared statements. ### Planned Features -- Positional and named arguments. +- Named arguments. - Embedded replicas. -- Prepared statements. - Batched statements. - Transactions. @@ -45,9 +46,21 @@ await dbClient.Execute("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, h Using positional arguments ```csharp -await dbClient.Execute("SELECT name FROM users WHERE id = ?", userId); +await dbClient.Query("SELECT name FROM users WHERE id = ?", userId); +``` + +Using prepared statements +```csharp +var statement = dbClient.Prepare("SELECT name FROM users WHERE id = ?"); +await statement.Query(userId); ``` +#### `Execute` vs. `Query` + +`Execute` returns only the number of affected rows and is intended for statements where further detail is not necessary. + +`Query` returns a more useful `IResultSet` object which can be read for additional information such as the number of affected rows, the last inserted row ID, the column names, and the rows themselves. + ### Querying the Database ```csharp @@ -66,11 +79,35 @@ User ToUser(IEnumerable row) throw new ArgumentException(); } -var result = await dbClient.Execute("SELECT * FROM users"); +var result = await dbClient.Query("SELECT * FROM users"); var users = result.Rows.Select(ToUser); ``` +### Prepared Statements + +The following creates a prepared statement. +```csharp +var statement = dbClient.Prepare("SELECT * FROM users where userId = ?"); +``` +You can then bind positional arguments in order. +```csharp +statement.Bind(new Integer(1)) +``` +Statement execution can be done in two ways, both are the same. +```csharp +IResultSet result; +result = statement.Query(); +result = dbClient.Query(statement); +``` +You can also query the number of values already bound to the statement. +```csharp +statement.Bind(0.5); +statement.Bind("libsql"); +var numberOfBoundValues = statement.BoundValuesCount; +Console.WriteLine(numberOfBoundValues) // 2 +``` + ### Closing the Database ```csharp From ff64e6352e1dd5f083385a25e1ea270c426672f8 Mon Sep 17 00:00:00 2001 From: Tom van Dinther <39470469+tvandinther@users.noreply.github.com> Date: Sat, 31 Aug 2024 18:19:40 +0200 Subject: [PATCH 08/16] Remove writelines --- Libsql.Client.Tests/ExecuteTests.cs | 1 - Libsql.Client/StatementWrapper.cs | 5 ----- 2 files changed, 6 deletions(-) diff --git a/Libsql.Client.Tests/ExecuteTests.cs b/Libsql.Client.Tests/ExecuteTests.cs index 0c78d1a..6458283 100644 --- a/Libsql.Client.Tests/ExecuteTests.cs +++ b/Libsql.Client.Tests/ExecuteTests.cs @@ -8,7 +8,6 @@ public class ExecuteTests public async Task CreateTable_NoRowsAffected() { var rowsAffected = await _db.Execute("CREATE TABLE `test` (`id` INTEGER PRIMARY KEY AUTOINCREMENT)"); - Console.WriteLine(rowsAffected); Assert.Equal(0ul, rowsAffected); } diff --git a/Libsql.Client/StatementWrapper.cs b/Libsql.Client/StatementWrapper.cs index 713fe66..8c97eb8 100644 --- a/Libsql.Client/StatementWrapper.cs +++ b/Libsql.Client/StatementWrapper.cs @@ -74,7 +74,6 @@ private unsafe void BindInt(int value) { var error = new Error(); var index = _bindIndex; - Console.WriteLine($"Binding int at index {index}"); var exitCode = Bindings.libsql_bind_int(Stmt, index, value, &error.Ptr); error.ThrowIfNonZero(exitCode, $"Failed to bind integer at index {index} with value {value}"); @@ -85,7 +84,6 @@ private unsafe void BindFloat(double value) { var error = new Error(); var index = _bindIndex; - Console.WriteLine($"Binding int at index {index}"); var exitCode = Bindings.libsql_bind_float(Stmt, index, value, &error.Ptr); error.ThrowIfNonZero(exitCode, $"Failed to bind integer at index {index} with value {value}"); @@ -96,7 +94,6 @@ private unsafe void BindString(string value) { var error = new Error(); var index = _bindIndex; - Console.WriteLine($"Binding int at index {index}"); fixed (byte* sPtr = Encoding.UTF8.GetBytes(value)) { var exitCode = Bindings.libsql_bind_string(Stmt, index, sPtr, &error.Ptr); @@ -109,7 +106,6 @@ private unsafe void BindBlob(byte[] value) { var error = new Error(); var index = _bindIndex; - Console.WriteLine($"Binding int at index {index}"); fixed (byte* bPtr = value) { var exitCode = Bindings.libsql_bind_blob(Stmt, index, bPtr, value.Length, &error.Ptr); @@ -122,7 +118,6 @@ private unsafe void Bind() { var error = new Error(); var index = _bindIndex; - Console.WriteLine($"Binding int at index {index}"); var exitCode = Bindings.libsql_bind_null(Stmt, index, &error.Ptr); error.ThrowIfNonZero(exitCode, $"Failed to bind null at index {index}"); From 3d2430efac5becc43a27322317df9e05d4c8d3be Mon Sep 17 00:00:00 2001 From: Tom van Dinther <39470469+tvandinther@users.noreply.github.com> Date: Sat, 31 Aug 2024 18:22:27 +0200 Subject: [PATCH 09/16] Add BoundValuesCount test --- Libsql.Client.Tests/StatementTests.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Libsql.Client.Tests/StatementTests.cs b/Libsql.Client.Tests/StatementTests.cs index 554c9fd..851fbbe 100644 --- a/Libsql.Client.Tests/StatementTests.cs +++ b/Libsql.Client.Tests/StatementTests.cs @@ -91,4 +91,17 @@ public async Task Statement_CanBind_Null() Assert.IsType(value); } + + [Fact] + public async Task Statement_BoundValuesCount_IsCorrect() + { + var statement = await _db.Prepare("SELECT ?, ?, ?"); + statement.Bind("One"); + statement.Bind("Two"); + statement.Bind("Three"); + + var numberOfBoundValues = statement.BoundValuesCount; + + Assert.Equal(3, numberOfBoundValues); + } } \ No newline at end of file From cc845cff4807d648e1ea67c42cf9873d0f398277 Mon Sep 17 00:00:00 2001 From: Tom van Dinther <39470469+tvandinther@users.noreply.github.com> Date: Sat, 31 Aug 2024 18:37:01 +0200 Subject: [PATCH 10/16] Fix prepared statement disposable bug --- Libsql.Client.Tests/StatementTests.cs | 14 ++++++------ Libsql.Client/DatabaseWrapper.cs | 32 ++++++++++++++++----------- Libsql.Client/IStatement.cs | 3 ++- Libsql.Client/StatementWrapper.cs | 25 ++++++++++++--------- 4 files changed, 42 insertions(+), 32 deletions(-) diff --git a/Libsql.Client.Tests/StatementTests.cs b/Libsql.Client.Tests/StatementTests.cs index 851fbbe..5fdb656 100644 --- a/Libsql.Client.Tests/StatementTests.cs +++ b/Libsql.Client.Tests/StatementTests.cs @@ -13,7 +13,7 @@ public StatementTests() [Fact] public async Task Prepare_ReturnedStatement_CanBeExecuted() { - var statement = await _db.Prepare("SELECT `id` FROM `test` WHERE `name` = ?"); + using var statement = await _db.Prepare("SELECT `id` FROM `test` WHERE `name` = ?"); Assert.NotNull(statement); Assert.IsAssignableFrom(statement); @@ -22,7 +22,7 @@ public async Task Prepare_ReturnedStatement_CanBeExecuted() [Fact] public async Task Statement_CanBind_Integer() { - var statement = await _db.Prepare("SELECT ?"); + using var statement = await _db.Prepare("SELECT ?"); var expected = 1; statement.Bind(new Integer(expected)); @@ -37,7 +37,7 @@ public async Task Statement_CanBind_Integer() [Fact] public async Task Statement_CanBind_Real() { - var statement = await _db.Prepare("SELECT ?"); + using var statement = await _db.Prepare("SELECT ?"); var expected = 0.5; statement.Bind(expected); @@ -52,7 +52,7 @@ public async Task Statement_CanBind_Real() [Fact] public async Task Statement_CanBind_Text() { - var statement = await _db.Prepare("SELECT ?"); + using var statement = await _db.Prepare("SELECT ?"); var expected = "hello"; statement.Bind(expected); @@ -67,7 +67,7 @@ public async Task Statement_CanBind_Text() [Fact] public async Task Statement_CanBind_Blob() { - var statement = await _db.Prepare("SELECT ?"); + using var statement = await _db.Prepare("SELECT ?"); var expected = new byte[]{30, 9, 42, 76}; statement.Bind(expected); @@ -82,7 +82,7 @@ public async Task Statement_CanBind_Blob() [Fact] public async Task Statement_CanBind_Null() { - var statement = await _db.Prepare("SELECT ?"); + using var statement = await _db.Prepare("SELECT ?"); statement.BindNull(); var rs = await statement.Query(); @@ -95,7 +95,7 @@ public async Task Statement_CanBind_Null() [Fact] public async Task Statement_BoundValuesCount_IsCorrect() { - var statement = await _db.Prepare("SELECT ?, ?, ?"); + using var statement = await _db.Prepare("SELECT ?, ?, ?"); statement.Bind("One"); statement.Bind("Two"); statement.Bind("Three"); diff --git a/Libsql.Client/DatabaseWrapper.cs b/Libsql.Client/DatabaseWrapper.cs index d757eab..bfe9f56 100644 --- a/Libsql.Client/DatabaseWrapper.cs +++ b/Libsql.Client/DatabaseWrapper.cs @@ -141,18 +141,22 @@ public Task Query(string sql) { return Task.Run(() => { - var statement = new StatementWrapper(this, _connection, sql); - return QueryStatement(statement); + using (var statement = new StatementWrapper(this, _connection, sql)) + { + return QueryStatement(statement); + } }); } public Task Query(string sql, params object[] args) { return Task.Run(() => { - var statement = new StatementWrapper(this, _connection, sql); - statement.BindAll(args); - - return QueryStatement(statement); + using (var statement = new StatementWrapper(this, _connection, sql)) + { + statement.BindAll(args); + + return QueryStatement(statement); + } }); } @@ -160,18 +164,22 @@ public Task Execute(string sql) { return Task.Run(() => { - var statement = new StatementWrapper(this, _connection, sql); - return ExecuteStatement(statement); + using (var statement = new StatementWrapper(this, _connection, sql)) + { + return ExecuteStatement(statement); + }; }); } public Task Execute(string sql, params object[] args) { return Task.Run(() => { - var statement = new StatementWrapper(this, _connection, sql); - statement.BindAll(args); + using (var statement = new StatementWrapper(this, _connection, sql)) + { + statement.BindAll(args); - return ExecuteStatement(statement); + return ExecuteStatement(statement); + } }); } @@ -207,7 +215,6 @@ private unsafe IResultSet QueryStatement(StatementWrapper statement) int exitCode; exitCode = Bindings.libsql_query_stmt(statement.Stmt, &rows, &error.Ptr); - statement.Dispose(); error.ThrowIfNonZero(exitCode, "Failed to execute statement"); @@ -225,7 +232,6 @@ private unsafe ulong ExecuteStatement(StatementWrapper statement) int exitCode; exitCode = Bindings.libsql_execute_stmt(statement.Stmt, &error.Ptr); - statement.Dispose(); error.ThrowIfNonZero(exitCode, "Failed to execute statement"); diff --git a/Libsql.Client/IStatement.cs b/Libsql.Client/IStatement.cs index 144dbf8..e6786bd 100644 --- a/Libsql.Client/IStatement.cs +++ b/Libsql.Client/IStatement.cs @@ -1,3 +1,4 @@ +using System; using System.Threading.Tasks; namespace Libsql.Client @@ -5,7 +6,7 @@ namespace Libsql.Client /// /// Represents the result set of a SQL query. /// - public interface IStatement + public interface IStatement : IDisposable { int BoundValuesCount { get; } void Bind(Integer integer); diff --git a/Libsql.Client/StatementWrapper.cs b/Libsql.Client/StatementWrapper.cs index 8c97eb8..54f2a3c 100644 --- a/Libsql.Client/StatementWrapper.cs +++ b/Libsql.Client/StatementWrapper.cs @@ -124,17 +124,6 @@ private unsafe void Bind() _bindIndex++; } - private void ReleaseUnmanagedResources() - { - Bindings.libsql_free_stmt(Stmt); - } - - public void Dispose() - { - ReleaseUnmanagedResources(); - GC.SuppressFinalize(this); - } - public void Bind(Integer integer) { BindInt(integer.Value); @@ -170,6 +159,20 @@ public Task Query() return _database.Query(this); } + private void ReleaseUnmanagedResources() + { + unsafe { + Console.WriteLine($"Freeing {(int) Stmt.ptr}"); + } + Bindings.libsql_free_stmt(Stmt); + } + + public void Dispose() + { + ReleaseUnmanagedResources(); + GC.SuppressFinalize(this); + } + ~StatementWrapper() { ReleaseUnmanagedResources(); From f3fcbe46de780b7ba59167415bea6edc6b53b0f8 Mon Sep 17 00:00:00 2001 From: Tom van Dinther <39470469+tvandinther@users.noreply.github.com> Date: Sat, 31 Aug 2024 18:39:10 +0200 Subject: [PATCH 11/16] Update README to address IDisposable on prepared statements --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 19f8e69..4d2a77d 100644 --- a/README.md +++ b/README.md @@ -107,6 +107,7 @@ statement.Bind("libsql"); var numberOfBoundValues = statement.BoundValuesCount; Console.WriteLine(numberOfBoundValues) // 2 ``` +> Prepared statements are held resources. `IStatement` implements the `IDisposable` interface. Make sure you manage its lifetime correctly. ### Closing the Database From 6391d13e4b49351e87036af5f93ce177e788628d Mon Sep 17 00:00:00 2001 From: Tom van Dinther <39470469+tvandinther@users.noreply.github.com> Date: Sat, 31 Aug 2024 18:40:59 +0200 Subject: [PATCH 12/16] Remove console.write --- Libsql.Client/StatementWrapper.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/Libsql.Client/StatementWrapper.cs b/Libsql.Client/StatementWrapper.cs index 54f2a3c..e8c1d60 100644 --- a/Libsql.Client/StatementWrapper.cs +++ b/Libsql.Client/StatementWrapper.cs @@ -161,9 +161,6 @@ public Task Query() private void ReleaseUnmanagedResources() { - unsafe { - Console.WriteLine($"Freeing {(int) Stmt.ptr}"); - } Bindings.libsql_free_stmt(Stmt); } From 6e5052a78c335e86c7b38ab26fcc3bf9d7c09d3e Mon Sep 17 00:00:00 2001 From: Tom van Dinther <39470469+tvandinther@users.noreply.github.com> Date: Sat, 31 Aug 2024 19:17:29 +0200 Subject: [PATCH 13/16] Add behaviour-finding tests and update README to reflect findings --- Libsql.Client.Tests/ExecuteTests.cs | 13 ++++++- Libsql.Client.Tests/ResultSetTests.cs | 16 ++++++++ Libsql.Client.Tests/RowsTests.cs | 42 +++++++++++++++++++++ Libsql.Client.Tests/StatementTests.cs | 53 +++++++++++++++++++-------- README.md | 2 +- 5 files changed, 109 insertions(+), 17 deletions(-) diff --git a/Libsql.Client.Tests/ExecuteTests.cs b/Libsql.Client.Tests/ExecuteTests.cs index 6458283..d54fac7 100644 --- a/Libsql.Client.Tests/ExecuteTests.cs +++ b/Libsql.Client.Tests/ExecuteTests.cs @@ -39,7 +39,7 @@ public async Task RowsAffected_ReturnsExpectedValue() [Fact] public async Task RowsAffected_ReturnsExectedValue_WhenMultipleUpdates() { - await _db.Query("CREATE TABLE `test` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `value` INTEGER)"); + await _db.Execute("CREATE TABLE `test` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `value` INTEGER)"); for (int i = 0; i < 10; i++) { @@ -50,4 +50,15 @@ public async Task RowsAffected_ReturnsExectedValue_WhenMultipleUpdates() Assert.Equal(10ul, rs2.RowsAffected); } + + [Fact] + public async Task Throws_WhenRowsReturned() + { + async Task action() + { + await _db.Execute("SELECT 1"); + } + + await Assert.ThrowsAsync(action); + } } diff --git a/Libsql.Client.Tests/ResultSetTests.cs b/Libsql.Client.Tests/ResultSetTests.cs index 84659de..e6b86f5 100644 --- a/Libsql.Client.Tests/ResultSetTests.cs +++ b/Libsql.Client.Tests/ResultSetTests.cs @@ -43,6 +43,14 @@ public async Task Columns_NamesAreMarshalled_WhenSeveral() Assert.Equal("column_two", columns.Last()); } + [Fact] + public async Task LastInsertRowId_Zero_WhenNonQuery() + { + var rs = await _db.Query("CREATE TABLE `test` (`id` INTEGER PRIMARY KEY AUTOINCREMENT)"); + + Assert.Equal(0, rs.LastInsertRowId); + } + [Fact] public async Task LastInsertRowId_ReturnsExpectedValue() { @@ -66,6 +74,14 @@ public async Task LastInsertRowId_ReturnsExpectedValue_WhenMultipleInserts() } } + [Fact] + public async Task RowsAffected_Zero_WhenNonQuery() + { + var rs = await _db.Query("CREATE TABLE `test` (`id` INTEGER PRIMARY KEY AUTOINCREMENT)"); + + Assert.Equal(0ul, rs.RowsAffected); + } + [Fact] public async Task RowsAffected_ReturnsExpectedValue() { diff --git a/Libsql.Client.Tests/RowsTests.cs b/Libsql.Client.Tests/RowsTests.cs index bfa47de..eabddd5 100644 --- a/Libsql.Client.Tests/RowsTests.cs +++ b/Libsql.Client.Tests/RowsTests.cs @@ -3,6 +3,48 @@ public class RowsTests { private readonly IDatabaseClient _db = DatabaseClient.Create().Result; + + [Fact] + public async Task IteratedRows_Throws_WhenCreate() + { + async Task>> Action() { + var rs = await _db.Query("CREATE TABLE `test` (`id` INTEGER PRIMARY KEY AUTOINCREMENT)"); + + return rs.Rows.ToList(); + } + + await Assert.ThrowsAsync(Action); + } + + [Fact] + public async Task IteratedRows_Throws_WhenCreateIfNotExists() + { + var rs = await _db.Query("CREATE TABLE IF NOT EXISTS `test` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT)"); + + Assert.Empty(rs.Rows); + } + + [Fact] + public async Task Rows_Empty_WhenInsert() + { + await _db.Query("CREATE TABLE `test` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT)"); + + var rs = await _db.Query("INSERT INTO `test` (`name`) VALUES ('libsql')"); + + Assert.Empty(rs.Rows); + } + + [Fact] + public async Task Rows_Empty_WhenUpdate() + { + await _db.Query("CREATE TABLE `test` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT)"); + var rs = await _db.Query("INSERT INTO `test` (`name`) VALUES ('libsql')"); + Assert.Equal(1ul, rs.RowsAffected); + + var rs2 = await _db.Query("UPDATE `test` SET `name` = 'libsql2' WHERE id = 1"); + + Assert.Empty(rs2.Rows); + } [Fact] public async Task Rows_WhenEmpty() diff --git a/Libsql.Client.Tests/StatementTests.cs b/Libsql.Client.Tests/StatementTests.cs index 5fdb656..52438b7 100644 --- a/Libsql.Client.Tests/StatementTests.cs +++ b/Libsql.Client.Tests/StatementTests.cs @@ -4,21 +4,6 @@ public class StatementTests { private readonly IDatabaseClient _db = DatabaseClient.Create().Result; - public StatementTests() - { - _db.Execute("CREATE TABLE `test` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT)"); - _db.Execute("INSERT INTO `test` VALUES ('a', 'b', 'c')"); - } - - [Fact] - public async Task Prepare_ReturnedStatement_CanBeExecuted() - { - using var statement = await _db.Prepare("SELECT `id` FROM `test` WHERE `name` = ?"); - - Assert.NotNull(statement); - Assert.IsAssignableFrom(statement); - } - [Fact] public async Task Statement_CanBind_Integer() { @@ -104,4 +89,42 @@ public async Task Statement_BoundValuesCount_IsCorrect() Assert.Equal(3, numberOfBoundValues); } + + [Fact] + public async Task Database_CanQuery_Statements() + { + using var statement = await _db.Prepare("SELECT ?"); + var expected = 1; + + statement.Bind(new Integer(expected)); + var rs = await _db.Query(statement); + var row = rs.Rows.First(); + var value = row.First(); + var integer = Assert.IsType(value); + + Assert.Equal(expected, integer); + } + + [Fact] + public async Task Statement_CanExecute() + { + await _db.Execute("CREATE TABLE `test` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT)"); + using var statement = await _db.Prepare("INSERT INTO `test` (`name`) VALUES ('a'), ('b'), ('c')"); + + var rowsAffected = await statement.Execute(); + + Assert.Equal(3ul, rowsAffected); + } + + + [Fact] + public async Task Database_CanExecute_Statements() + { + await _db.Execute("CREATE TABLE `test` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT)"); + using var statement = await _db.Prepare("INSERT INTO `test` (`name`) VALUES ('a'), ('b'), ('c')"); + + var rowsAffected = await _db.Execute(statement); + + Assert.Equal(3ul, rowsAffected); + } } \ No newline at end of file diff --git a/README.md b/README.md index 4d2a77d..72bf279 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ await statement.Query(userId); #### `Execute` vs. `Query` -`Execute` returns only the number of affected rows and is intended for statements where further detail is not necessary. +`Execute` returns only the number of affected rows and is intended for statements where further detail is not necessary. A `LibSqlException` will be thrown if you use `Execute` on a statement that returns rows. `Query` returns a more useful `IResultSet` object which can be read for additional information such as the number of affected rows, the last inserted row ID, the column names, and the rows themselves. From 85e5e7b03c7e2bbaadb6adba4685110feb1382d4 Mon Sep 17 00:00:00 2001 From: Tom van Dinther <39470469+tvandinther@users.noreply.github.com> Date: Sat, 31 Aug 2024 21:52:13 +0200 Subject: [PATCH 14/16] Implement statement reset --- Libsql.Client.Tests/StatementTests.cs | 29 +++++++++++++++++++++++++++ Libsql.Client/IStatement.cs | 1 + Libsql.Client/Rows.cs | 2 +- Libsql.Client/StatementWrapper.cs | 15 ++++++++++++++ 4 files changed, 46 insertions(+), 1 deletion(-) diff --git a/Libsql.Client.Tests/StatementTests.cs b/Libsql.Client.Tests/StatementTests.cs index 52438b7..ae28f3e 100644 --- a/Libsql.Client.Tests/StatementTests.cs +++ b/Libsql.Client.Tests/StatementTests.cs @@ -127,4 +127,33 @@ public async Task Database_CanExecute_Statements() Assert.Equal(3ul, rowsAffected); } + + [Fact] + public async Task Statement_CanBeReset() + { + await _db.Execute("CREATE TABLE `test` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT)"); + await _db.Execute("INSERT INTO `test` (`name`) VALUES ('a'), ('b'), ('c')"); + + using var statement = await _db.Prepare("SELECT `name` FROM `test` WHERE `id` = ?"); + var firstExpected = "a"; + var secondExpected = "b"; + + statement.Bind(new Integer(1)); + var rs = await statement.Query(); + var row = rs.Rows.First(); + var value = row.First(); + var text = Assert.IsType(value); + + Assert.Equal(firstExpected, text); + + statement.Reset(); + + statement.Bind(new Integer(2)); + var rs2 = await statement.Query(); + var row2 = rs2.Rows.First(); + var value2 = row2.First(); + var text2 = Assert.IsType(value2); + + Assert.Equal(secondExpected, text2); + } } \ No newline at end of file diff --git a/Libsql.Client/IStatement.cs b/Libsql.Client/IStatement.cs index e6786bd..d81b12b 100644 --- a/Libsql.Client/IStatement.cs +++ b/Libsql.Client/IStatement.cs @@ -17,5 +17,6 @@ public interface IStatement : IDisposable void BindNull(); Task Execute(); Task Query(); + void Reset(); } } diff --git a/Libsql.Client/Rows.cs b/Libsql.Client/Rows.cs index c26fb53..8bcae46 100644 --- a/Libsql.Client/Rows.cs +++ b/Libsql.Client/Rows.cs @@ -123,7 +123,7 @@ private unsafe Value[] ParseRow(libsql_row_t row) _enumeratorData.ColumnTypes[i] == ValueType.Text ? row.GetText(i) : _enumeratorData.ColumnTypes[i] == ValueType.Blob ? row.GetBlob(i) : _enumeratorData.ColumnTypes[i] == ValueType.Null ? (Value)new Null() : - throw new ArgumentOutOfRangeException(); + throw new ArgumentOutOfRangeException($"Non-exhaustive check. Could not find a case to match value of {_enumeratorData.ColumnTypes[i]}"); parsedRow[i] = value; } diff --git a/Libsql.Client/StatementWrapper.cs b/Libsql.Client/StatementWrapper.cs index e8c1d60..a73e021 100644 --- a/Libsql.Client/StatementWrapper.cs +++ b/Libsql.Client/StatementWrapper.cs @@ -159,6 +159,21 @@ public Task Query() return _database.Query(this); } + public void Reset() + { + var error = new Error(); + int exitCode; + + unsafe { + exitCode = Bindings.libsql_reset_stmt(Stmt, &error.Ptr); + } + + _bindIndex = 1; + + error.ThrowIfNonZero(exitCode, "Failed to reset statement"); + } + + private void ReleaseUnmanagedResources() { Bindings.libsql_free_stmt(Stmt); From 7045a122dc0224bfb15b7476c2d997c79b438037 Mon Sep 17 00:00:00 2001 From: Tom van Dinther <39470469+tvandinther@users.noreply.github.com> Date: Thu, 5 Sep 2024 23:01:30 +0200 Subject: [PATCH 15/16] Add index argument to bind methods and add xmldoc --- Libsql.Client.Tests/StatementTests.cs | 37 +++++++++++++------ Libsql.Client/IDatabaseClient.cs | 6 ++++ Libsql.Client/IStatement.cs | 51 +++++++++++++++++++++++---- Libsql.Client/StatementWrapper.cs | 48 ++++++++++++------------- 4 files changed, 98 insertions(+), 44 deletions(-) diff --git a/Libsql.Client.Tests/StatementTests.cs b/Libsql.Client.Tests/StatementTests.cs index ae28f3e..7e6d182 100644 --- a/Libsql.Client.Tests/StatementTests.cs +++ b/Libsql.Client.Tests/StatementTests.cs @@ -10,7 +10,7 @@ public async Task Statement_CanBind_Integer() using var statement = await _db.Prepare("SELECT ?"); var expected = 1; - statement.Bind(new Integer(expected)); + statement.Bind(new Integer(expected), 1); var rs = await statement.Query(); var row = rs.Rows.First(); var value = row.First(); @@ -25,7 +25,7 @@ public async Task Statement_CanBind_Real() using var statement = await _db.Prepare("SELECT ?"); var expected = 0.5; - statement.Bind(expected); + statement.Bind(expected, 1); var rs = await statement.Query(); var row = rs.Rows.First(); var value = row.First(); @@ -40,7 +40,7 @@ public async Task Statement_CanBind_Text() using var statement = await _db.Prepare("SELECT ?"); var expected = "hello"; - statement.Bind(expected); + statement.Bind(expected, 1); var rs = await statement.Query(); var row = rs.Rows.First(); var value = row.First(); @@ -55,7 +55,7 @@ public async Task Statement_CanBind_Blob() using var statement = await _db.Prepare("SELECT ?"); var expected = new byte[]{30, 9, 42, 76}; - statement.Bind(expected); + statement.Bind(expected, 1); var rs = await statement.Query(); var row = rs.Rows.First(); var value = row.First(); @@ -69,7 +69,7 @@ public async Task Statement_CanBind_Null() { using var statement = await _db.Prepare("SELECT ?"); - statement.BindNull(); + statement.BindNull(1); var rs = await statement.Query(); var row = rs.Rows.First(); var value = row.First(); @@ -77,13 +77,28 @@ public async Task Statement_CanBind_Null() Assert.IsType(value); } + [Fact] + public async Task Statement_CanBind_NNNIndex() + { + using var statement = await _db.Prepare("SELECT ?123"); + var expected = "Parameter annotated as the 123rd position"; + + statement.Bind(expected, 123); + var rs = await statement.Query(); + var row = rs.Rows.First(); + var value = row.First(); + + var text = Assert.IsType(value); + Assert.Equal(expected, text.Value); + } + [Fact] public async Task Statement_BoundValuesCount_IsCorrect() { using var statement = await _db.Prepare("SELECT ?, ?, ?"); - statement.Bind("One"); - statement.Bind("Two"); - statement.Bind("Three"); + statement.Bind("One", 1); + statement.Bind("Two", 2); + statement.Bind("Three", 3); var numberOfBoundValues = statement.BoundValuesCount; @@ -96,7 +111,7 @@ public async Task Database_CanQuery_Statements() using var statement = await _db.Prepare("SELECT ?"); var expected = 1; - statement.Bind(new Integer(expected)); + statement.Bind(new Integer(expected), 1); var rs = await _db.Query(statement); var row = rs.Rows.First(); var value = row.First(); @@ -138,7 +153,7 @@ public async Task Statement_CanBeReset() var firstExpected = "a"; var secondExpected = "b"; - statement.Bind(new Integer(1)); + statement.Bind(new Integer(1), 1); var rs = await statement.Query(); var row = rs.Rows.First(); var value = row.First(); @@ -148,7 +163,7 @@ public async Task Statement_CanBeReset() statement.Reset(); - statement.Bind(new Integer(2)); + statement.Bind(new Integer(2), 1); var rs2 = await statement.Query(); var row2 = rs2.Rows.First(); var value2 = row2.First(); diff --git a/Libsql.Client/IDatabaseClient.cs b/Libsql.Client/IDatabaseClient.cs index 26e1a59..7458fec 100644 --- a/Libsql.Client/IDatabaseClient.cs +++ b/Libsql.Client/IDatabaseClient.cs @@ -13,6 +13,7 @@ public interface IDatabaseClient /// The SQL query to execute. /// The result set returned by the query. /// Thrown when the query fails to execute. + /// Use if your prepared statement does not return rows. Task Query(string sql); /// @@ -22,6 +23,7 @@ public interface IDatabaseClient /// The parameters to use in the query. /// The result set returned by the query. /// Thrown when the query fails to execute. + /// Use if your prepared statement does not return rows. Task Query(string sql, params object[] args); /// @@ -30,6 +32,7 @@ public interface IDatabaseClient /// The prepared statement to execute. /// The result set returned by the prepared statement. /// Thrown when the prepared statement fails to execute. + /// Use if your prepared statement does not return rows. Task Query(IStatement statement); /// @@ -38,6 +41,7 @@ public interface IDatabaseClient /// The SQL query to execute. /// The number of affected rows. /// Thrown when the SQL fails to execute. + /// Use if your prepared statement returns rows. Task Execute(string sql); /// @@ -47,6 +51,7 @@ public interface IDatabaseClient /// The parameters to use in the query. /// The number of affected rows. /// Thrown when the SQL fails to execute. + /// Use if your prepared statement returns rows. Task Execute(string sql, params object[] args); /// @@ -55,6 +60,7 @@ public interface IDatabaseClient /// The prepared statement to execute. /// The number of affected rows. /// Thrown when the prepared statement fails to execute. + /// Use if your prepared statement returns rows. Task Execute(IStatement statement); /// diff --git a/Libsql.Client/IStatement.cs b/Libsql.Client/IStatement.cs index d81b12b..fdb4e39 100644 --- a/Libsql.Client/IStatement.cs +++ b/Libsql.Client/IStatement.cs @@ -4,19 +4,56 @@ namespace Libsql.Client { /// - /// Represents the result set of a SQL query. + /// A prepared statement. /// public interface IStatement : IDisposable { + /// + /// Gets the number of values currently bound on the prepared statement. + /// int BoundValuesCount { get; } - void Bind(Integer integer); - - void Bind(Real real); - void Bind(Text text); - void Bind(Blob blob); - void BindNull(); + + /// + /// Binds a value to the prepared statement. + /// + /// The value to bind. + /// The positional index to bind the value to. + /// The leftmost parameter has an index of 1. + /// If ?NNN syntax is used, the index is the value of NNN. + void Bind(Integer integer, int index); + + /// + /// The value to bind. + void Bind(Real real, int index); + + /// + /// The value to bind. + void Bind(Text text, int index); + + /// + /// The value to bind. + void Bind(Blob blob, int index); + + /// + /// + /// Binds a value to the prepared statement. + /// + void BindNull(int index); + + /// + /// You may also call this method with the prepared statement as the argument. + /// Use if your prepared statement returns rows. Task Execute(); + + /// + /// You may also call this method with the prepared statement as the argument. + /// Use if your prepared statement does not return rows. Task Query(); + + /// + /// Resets the prepared statement so that it can be executed again. + /// + /// Bound parameters are cleared. void Reset(); } } diff --git a/Libsql.Client/StatementWrapper.cs b/Libsql.Client/StatementWrapper.cs index a73e021..51bb663 100644 --- a/Libsql.Client/StatementWrapper.cs +++ b/Libsql.Client/StatementWrapper.cs @@ -44,24 +44,25 @@ public unsafe void BindAll(object[] values) } else { - foreach (var arg in values) + for (int i = 1; i <= values.Length; i++) { + object arg = values[i - 1]; switch (arg) { case int val: - BindInt(val); + BindInt(val, i); break; case double d: - BindFloat(d); + BindFloat(d, i); break; case string s: - BindString(s); + BindString(s, i); break; case byte[] b: - BindBlob(b); + BindBlob(b, i); break; case null: - Bind(); + Bind(i); break; default: throw new ArgumentException($"Unsupported argument type: {arg.GetType()}"); @@ -70,30 +71,27 @@ public unsafe void BindAll(object[] values) } } - private unsafe void BindInt(int value) + private unsafe void BindInt(int value, int index) { var error = new Error(); - var index = _bindIndex; var exitCode = Bindings.libsql_bind_int(Stmt, index, value, &error.Ptr); error.ThrowIfNonZero(exitCode, $"Failed to bind integer at index {index} with value {value}"); _bindIndex++; } - private unsafe void BindFloat(double value) + private unsafe void BindFloat(double value, int index) { var error = new Error(); - var index = _bindIndex; var exitCode = Bindings.libsql_bind_float(Stmt, index, value, &error.Ptr); error.ThrowIfNonZero(exitCode, $"Failed to bind integer at index {index} with value {value}"); _bindIndex++; } - private unsafe void BindString(string value) + private unsafe void BindString(string value, int index) { var error = new Error(); - var index = _bindIndex; fixed (byte* sPtr = Encoding.UTF8.GetBytes(value)) { var exitCode = Bindings.libsql_bind_string(Stmt, index, sPtr, &error.Ptr); @@ -102,10 +100,9 @@ private unsafe void BindString(string value) _bindIndex++; } - private unsafe void BindBlob(byte[] value) + private unsafe void BindBlob(byte[] value, int index) { var error = new Error(); - var index = _bindIndex; fixed (byte* bPtr = value) { var exitCode = Bindings.libsql_bind_blob(Stmt, index, bPtr, value.Length, &error.Ptr); @@ -114,39 +111,38 @@ private unsafe void BindBlob(byte[] value) _bindIndex++; } - private unsafe void Bind() + private unsafe void Bind(int index) { var error = new Error(); - var index = _bindIndex; var exitCode = Bindings.libsql_bind_null(Stmt, index, &error.Ptr); error.ThrowIfNonZero(exitCode, $"Failed to bind null at index {index}"); _bindIndex++; } - public void Bind(Integer integer) + public void Bind(Integer integer, int index) { - BindInt(integer.Value); + BindInt(integer.Value, index); } - public void Bind(Real real) + public void Bind(Real real, int index) { - BindFloat(real.Value); + BindFloat(real.Value, index); } - public void Bind(Text text) + public void Bind(Text text, int index) { - BindString(text.Value); + BindString(text.Value, index); } - public void Bind(Blob blob) + public void Bind(Blob blob, int index) { - BindBlob(blob.Value); + BindBlob(blob.Value, index); } - public void BindNull() + public void BindNull(int index) { - Bind(); + Bind(index); } public Task Execute() From b4fdef5d39b7cf55e34781769e8e2915150bc530 Mon Sep 17 00:00:00 2001 From: Tom van Dinther <39470469+tvandinther@users.noreply.github.com> Date: Thu, 5 Sep 2024 23:50:00 +0200 Subject: [PATCH 16/16] Implement parameter count --- Libsql.Client.Tests/StatementTests.cs | 18 ++++++++++++++++++ Libsql.Client/IStatement.cs | 9 +++++++++ Libsql.Client/StatementWrapper.cs | 14 ++++++++++++++ 3 files changed, 41 insertions(+) diff --git a/Libsql.Client.Tests/StatementTests.cs b/Libsql.Client.Tests/StatementTests.cs index 7e6d182..e50e1f1 100644 --- a/Libsql.Client.Tests/StatementTests.cs +++ b/Libsql.Client.Tests/StatementTests.cs @@ -92,6 +92,24 @@ public async Task Statement_CanBind_NNNIndex() Assert.Equal(expected, text.Value); } + [Theory] + [InlineData("SELECT 1", 0)] + [InlineData("SELECT ?", 1)] + [InlineData("SELECT ?, ?", 2)] + [InlineData("SELECT ?, ?, ?", 3)] + [InlineData("SELECT ?10, ?11, ?33", 33)] + [InlineData("SELECT ?10, ?33, ?11", 33)] + [InlineData("SELECT ?, ?, ?2", 2)] + [InlineData("SELECT ?, ?, ?2, ?, ?", 4)] + public async Task Statement_CanGetParameterCount(string query, int expected) + { + using var statement = await _db.Prepare(query); + + var parameterCount = statement.ParameterCount; + + Assert.Equal(expected, parameterCount); + } + [Fact] public async Task Statement_BoundValuesCount_IsCorrect() { diff --git a/Libsql.Client/IStatement.cs b/Libsql.Client/IStatement.cs index fdb4e39..403e2b6 100644 --- a/Libsql.Client/IStatement.cs +++ b/Libsql.Client/IStatement.cs @@ -13,6 +13,15 @@ public interface IStatement : IDisposable /// int BoundValuesCount { get; } + /// + /// Gets the number of parameters in the prepared statement. + /// + /// + /// If ?NNN syntax is used, the Parameter count is the maximum value of + /// NNN used plus any unannotated positional parameters to the right. + /// + int ParameterCount { get; } + /// /// Binds a value to the prepared statement. /// diff --git a/Libsql.Client/StatementWrapper.cs b/Libsql.Client/StatementWrapper.cs index 51bb663..e5988dc 100644 --- a/Libsql.Client/StatementWrapper.cs +++ b/Libsql.Client/StatementWrapper.cs @@ -8,6 +8,9 @@ namespace Libsql.Client internal class StatementWrapper : IStatement { int IStatement.BoundValuesCount => _bindIndex - 1; + + public int ParameterCount => GetParameterCount(); + private int _bindIndex = 1; public readonly libsql_stmt_t Stmt; private readonly libsql_connection_t _connection; @@ -120,6 +123,17 @@ private unsafe void Bind(int index) _bindIndex++; } + private unsafe int GetParameterCount() + { + var error = new Error(); + int count; + var exitCode = Bindings.libsql_stmt_parameter_count(Stmt, &count,&error.Ptr); + + error.ThrowIfNonZero(exitCode, $"Failed to get parameter count"); + + return count; + } + public void Bind(Integer integer, int index) { BindInt(integer.Value, index);