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..d54fac7 --- /dev/null +++ b/Libsql.Client.Tests/ExecuteTests.cs @@ -0,0 +1,64 @@ +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)"); + + 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.Execute("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); + } + + [Fact] + public async Task Throws_WhenRowsReturned() + { + async Task action() + { + await _db.Execute("SELECT 1"); + } + + await Assert.ThrowsAsync(action); + } +} 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..e6b86f5 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()); @@ -43,12 +43,20 @@ 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() { 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 +69,31 @@ 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_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() { 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 +102,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..eabddd5 100644 --- a/Libsql.Client.Tests/RowsTests.cs +++ b/Libsql.Client.Tests/RowsTests.cs @@ -3,11 +3,53 @@ 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() { - 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 +58,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 +69,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.Tests/StatementTests.cs b/Libsql.Client.Tests/StatementTests.cs new file mode 100644 index 0000000..e50e1f1 --- /dev/null +++ b/Libsql.Client.Tests/StatementTests.cs @@ -0,0 +1,192 @@ +namespace Libsql.Client.Tests; + +public class StatementTests +{ + private readonly IDatabaseClient _db = DatabaseClient.Create().Result; + + [Fact] + public async Task Statement_CanBind_Integer() + { + using var statement = await _db.Prepare("SELECT ?"); + var expected = 1; + + statement.Bind(new Integer(expected), 1); + var rs = await statement.Query(); + var row = rs.Rows.First(); + var value = row.First(); + var integer = Assert.IsType(value); + + Assert.Equal(expected, integer); + } + + [Fact] + public async Task Statement_CanBind_Real() + { + using var statement = await _db.Prepare("SELECT ?"); + var expected = 0.5; + + statement.Bind(expected, 1); + var rs = await statement.Query(); + var row = rs.Rows.First(); + var value = row.First(); + var integer = Assert.IsType(value); + + Assert.Equal(expected, integer); + } + + [Fact] + public async Task Statement_CanBind_Text() + { + using var statement = await _db.Prepare("SELECT ?"); + var expected = "hello"; + + statement.Bind(expected, 1); + var rs = await statement.Query(); + var row = rs.Rows.First(); + var value = row.First(); + var text = Assert.IsType(value); + + Assert.Equal(expected, text); + } + + [Fact] + public async Task Statement_CanBind_Blob() + { + using var statement = await _db.Prepare("SELECT ?"); + var expected = new byte[]{30, 9, 42, 76}; + + statement.Bind(expected, 1); + var rs = await statement.Query(); + var row = rs.Rows.First(); + var value = row.First(); + var blob = Assert.IsType(value); + + Assert.Equal(expected, blob); + } + + [Fact] + public async Task Statement_CanBind_Null() + { + using var statement = await _db.Prepare("SELECT ?"); + + statement.BindNull(1); + var rs = await statement.Query(); + var row = rs.Rows.First(); + var value = row.First(); + + 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); + } + + [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() + { + using var statement = await _db.Prepare("SELECT ?, ?, ?"); + statement.Bind("One", 1); + statement.Bind("Two", 2); + statement.Bind("Three", 3); + + var numberOfBoundValues = statement.BoundValuesCount; + + 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), 1); + 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); + } + + [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), 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), 1); + 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/DatabaseWrapper.cs b/Libsql.Client/DatabaseWrapper.cs index d8d6cc4..bfe9f56 100644 --- a/Libsql.Client/DatabaseWrapper.cs +++ b/Libsql.Client/DatabaseWrapper.cs @@ -136,35 +136,85 @@ internal unsafe void Connect() error.ThrowIfNonZero(exitCode, "Failed to connect to database"); } + + public Task Query(string sql) + { + return Task.Run(() => + { + using (var statement = new StatementWrapper(this, _connection, sql)) + { + return QueryStatement(statement); + } + }); + } + + public Task Query(string sql, params object[] args) + { + return Task.Run(() => { + using (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); - return ExecuteStatement(statement); + using (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(() => { + using (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); }); } - // TODO: Differentiate query statements and execute statements. - private unsafe IResultSet ExecuteStatement(Statement statement) + + private unsafe IResultSet QueryStatement(StatementWrapper statement) { var error = new Error(); var rows = new libsql_rows_t(); int exitCode; exitCode = Bindings.libsql_query_stmt(statement.Stmt, &rows, &error.Ptr); - statement.Dispose(); error.ThrowIfNonZero(exitCode, "Failed to execute statement"); @@ -176,14 +226,26 @@ private unsafe IResultSet ExecuteStatement(Statement statement) ); } - public async Task Sync() + private unsafe ulong ExecuteStatement(StatementWrapper statement) + { + var error = new Error(); + int exitCode; + + exitCode = Bindings.libsql_execute_stmt(statement.Stmt, &error.Ptr); + + error.ThrowIfNonZero(exitCode, "Failed to execute statement"); + + return Bindings.libsql_changes(_connection); + } + + public Task Sync() { if (_type != DatabaseType.EmbeddedReplica) { throw new InvalidOperationException("Cannot sync a non-replica database"); } - await Task.Run(() => + return Task.Run(() => { unsafe { @@ -197,6 +259,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 f84d023..7458fec 100644 --- a/Libsql.Client/IDatabaseClient.cs +++ b/Libsql.Client/IDatabaseClient.cs @@ -13,7 +13,8 @@ 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); + /// Use if your prepared statement does not return rows. + Task Query(string sql); /// /// Executes the given SQL query with the specified parameters and returns the result set. @@ -22,7 +23,52 @@ 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); + /// Use if your prepared statement does not return rows. + 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. + /// Use if your prepared statement does not return rows. + 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. + /// Use if your prepared statement returns rows. + Task Execute(string sql); + + /// + /// Executes the given SQL with the specified parameters. + /// + /// The SQL query to execute. + /// 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); + + /// + /// Executes the given prepared statement. + /// + /// 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); + + /// + /// 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..403e2b6 --- /dev/null +++ b/Libsql.Client/IStatement.cs @@ -0,0 +1,68 @@ +using System; +using System.Threading.Tasks; + +namespace Libsql.Client +{ + /// + /// A prepared statement. + /// + public interface IStatement : IDisposable + { + /// + /// Gets the number of values currently bound on the prepared statement. + /// + 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. + /// + /// 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/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/Statement.cs b/Libsql.Client/Statement.cs deleted file mode 100644 index a261f89..0000000 --- a/Libsql.Client/Statement.cs +++ /dev/null @@ -1,94 +0,0 @@ -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(); - } - } -} diff --git a/Libsql.Client/StatementWrapper.cs b/Libsql.Client/StatementWrapper.cs new file mode 100644 index 0000000..e5988dc --- /dev/null +++ b/Libsql.Client/StatementWrapper.cs @@ -0,0 +1,203 @@ +using System; +using System.Diagnostics; +using System.Text; +using System.Threading.Tasks; + +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; + 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 + { + for (int i = 1; i <= values.Length; i++) + { + object arg = values[i - 1]; + switch (arg) + { + case int val: + BindInt(val, i); + break; + case double d: + BindFloat(d, i); + break; + case string s: + BindString(s, i); + break; + case byte[] b: + BindBlob(b, i); + break; + case null: + Bind(i); + break; + default: + throw new ArgumentException($"Unsupported argument type: {arg.GetType()}"); + } + } + } + } + + private unsafe void BindInt(int value, int index) + { + var error = new Error(); + 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, int index) + { + var error = new Error(); + 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, int index) + { + var error = new Error(); + 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, int index) + { + var error = new Error(); + 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(int index) + { + var error = new Error(); + var exitCode = Bindings.libsql_bind_null(Stmt, index, &error.Ptr); + + error.ThrowIfNonZero(exitCode, $"Failed to bind null at index {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); + } + + public void Bind(Real real, int index) + { + BindFloat(real.Value, index); + } + + public void Bind(Text text, int index) + { + BindString(text.Value, index); + } + + public void Bind(Blob blob, int index) + { + BindBlob(blob.Value, index); + } + + public void BindNull(int index) + { + Bind(index); + } + + public Task Execute() + { + return _database.Execute(this); + } + + 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); + } + + public void Dispose() + { + ReleaseUnmanagedResources(); + GC.SuppressFinalize(this); + } + + ~StatementWrapper() + { + ReleaseUnmanagedResources(); + } + } +} 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) diff --git a/Libsql.Client/Values/Integer.cs b/Libsql.Client/Values/Integer.cs index 3e68dd7..b1c0851 100644 --- a/Libsql.Client/Values/Integer.cs +++ b/Libsql.Client/Values/Integer.cs @@ -23,6 +23,10 @@ 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) 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) 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) { diff --git a/README.md b/README.md index e973069..72bf279 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. 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. + ### Querying the Database ```csharp @@ -66,11 +79,36 @@ 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 +``` +> Prepared statements are held resources. `IStatement` implements the `IDisposable` interface. Make sure you manage its lifetime correctly. + ### Closing the Database ```csharp