From d35a8c75c55a305eed27ee2b0b7663b2c181cdaf 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/28] 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 6da97a2..4df8d7f 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 8303345..9daaef9 100644 --- a/Libsql.Client.Tests/PositionalArgumentTests.cs +++ b/Libsql.Client.Tests/PositionalArgumentTests.cs @@ -7,7 +7,7 @@ public class PositionalArgumentTests : IDisposable [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 49174a4..bf155a9 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 6f71fde..0d8b72f 100644 --- a/Libsql.Client.Tests/ResultSetTests.cs +++ b/Libsql.Client.Tests/ResultSetTests.cs @@ -7,7 +7,7 @@ public class ResultSetTests : IDisposable [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 0ac54f3..11d6cd9 100644 --- a/Libsql.Client.Tests/RowsTests.cs +++ b/Libsql.Client.Tests/RowsTests.cs @@ -7,7 +7,7 @@ public class RowsTests : IDisposable [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 a812dec..eb5c6fc 100644 --- a/Libsql.Client.Tests/SelectTests.cs +++ b/Libsql.Client.Tests/SelectTests.cs @@ -7,7 +7,7 @@ public class SelectTests : IDisposable [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 68bb3d8..011be74 100644 --- a/Libsql.Client/DatabaseWrapper.cs +++ b/Libsql.Client/DatabaseWrapper.cs @@ -139,8 +139,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(() => { @@ -149,7 +168,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); @@ -159,8 +178,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(); @@ -179,6 +197,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 3285d55..50a5722 100644 --- a/Libsql.Client/IDatabaseClient.cs +++ b/Libsql.Client/IDatabaseClient.cs @@ -14,7 +14,7 @@ public interface IDatabaseClient : IDisposable /// 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. @@ -23,7 +23,22 @@ public interface IDatabaseClient : IDisposable /// 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 dc21606432472b071566a8a9c41b23f17cb611a9 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/28] 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 011be74..c117a7c 100644 --- a/Libsql.Client/DatabaseWrapper.cs +++ b/Libsql.Client/DatabaseWrapper.cs @@ -140,45 +140,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(); @@ -197,7 +222,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; @@ -210,14 +235,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 { @@ -231,6 +256,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 50a5722..1b6ac11 100644 --- a/Libsql.Client/IDatabaseClient.cs +++ b/Libsql.Client/IDatabaseClient.cs @@ -25,10 +25,19 @@ public interface IDatabaseClient : IDisposable /// 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); @@ -37,9 +46,25 @@ public interface IDatabaseClient : IDisposable /// /// 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 9c070649f922b8cb6d97cda60bc39c86c34d5702 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/28] 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 8c8405df3e3c5b0988b1c3cadad2d7edfb3fcc74 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/28] 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 602537c4f85962c9db4ab9872fc12de485d4e11e 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/28] 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 c15febc0ef3d74d2467b6b27e639f5151dfac6ea 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/28] 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 ac1d18fc52677e841fbcb26607145016eae66370 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/28] 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 099c4c5..4a4fc47 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. @@ -89,9 +90,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 @@ -110,11 +123,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 578dd5a906e66b8e3a4693e4e3913ba3e42e034f 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/28] 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 be881028d6100210fd672f3b1d89d63e3bb161eb 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/28] 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 669a37e0ab08b77693ef8e27abb12a0ad57b5f90 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/28] 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 c117a7c..cd782c0 100644 --- a/Libsql.Client/DatabaseWrapper.cs +++ b/Libsql.Client/DatabaseWrapper.cs @@ -144,18 +144,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); + } }); } @@ -163,18 +167,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); + } }); } @@ -210,7 +218,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"); @@ -228,7 +235,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 a8f090fbd510ba910dbfb9188af6b6495b462470 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/28] 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 4a4fc47..359aa24 100644 --- a/README.md +++ b/README.md @@ -151,6 +151,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 2d389150d1fae9f6b19a76610a4a5b13756010c5 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/28] 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 33537fd8776f637329550323821854ddc6a6d99a 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/28] 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 0d8b72f..b1dcb80 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 11d6cd9..867c550 100644 --- a/Libsql.Client.Tests/RowsTests.cs +++ b/Libsql.Client.Tests/RowsTests.cs @@ -3,6 +3,48 @@ public class RowsTests : IDisposable { 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 359aa24..b056998 100644 --- a/README.md +++ b/README.md @@ -101,7 +101,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 931b4e633af744239e21d9f946baadb0062c6a91 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/28] 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 1595d32806bd30230a34f959890a7a516434ad26 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/28] 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 1b6ac11..8daf5ab 100644 --- a/Libsql.Client/IDatabaseClient.cs +++ b/Libsql.Client/IDatabaseClient.cs @@ -14,6 +14,7 @@ public interface IDatabaseClient : IDisposable /// 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); /// @@ -23,6 +24,7 @@ public interface IDatabaseClient : IDisposable /// 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); /// @@ -31,6 +33,7 @@ public interface IDatabaseClient : IDisposable /// 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); /// @@ -39,6 +42,7 @@ public interface IDatabaseClient : IDisposable /// 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); /// @@ -48,6 +52,7 @@ public interface IDatabaseClient : IDisposable /// 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); /// @@ -56,6 +61,7 @@ public interface IDatabaseClient : IDisposable /// 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 2fb7e3228cbf0f3007201284022ec0e05e588a5b 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/28] 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); From 8be03908affd00ac80a7c2d7a2b1a4f5fff0e0d0 Mon Sep 17 00:00:00 2001 From: Tom van Dinther <39470469+tvandinther@users.noreply.github.com> Date: Sat, 30 Aug 2025 12:46:20 +0200 Subject: [PATCH 17/28] WIP --- Libsql.Client.Tests/StatementTests.cs | 13 +++++++++ Libsql.Client/IStatement.cs | 3 +++ Libsql.Client/StatementWrapper.cs | 38 ++++++++++++++++++++++++++- generate-bindings.sh | 8 +++--- rust-bindings/Cargo.toml | 3 ++- 5 files changed, 59 insertions(+), 6 deletions(-) diff --git a/Libsql.Client.Tests/StatementTests.cs b/Libsql.Client.Tests/StatementTests.cs index e50e1f1..0c014c9 100644 --- a/Libsql.Client.Tests/StatementTests.cs +++ b/Libsql.Client.Tests/StatementTests.cs @@ -189,4 +189,17 @@ public async Task Statement_CanBeReset() Assert.Equal(secondExpected, text2); } + + [Fact] + public async Task Statement_CanGet_ColumnNames() + { + using var statement = await _db.Prepare("SELECT 1 AS `one`, 2 AS `two`"); + + var columnNames = statement.ColumnNames; + + Assert.Collection(columnNames, + r => Assert.Equal("one", r), + r => Assert.Equal("two", r) + ); + } } \ No newline at end of file diff --git a/Libsql.Client/IStatement.cs b/Libsql.Client/IStatement.cs index 403e2b6..95cd39f 100644 --- a/Libsql.Client/IStatement.cs +++ b/Libsql.Client/IStatement.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Threading.Tasks; namespace Libsql.Client @@ -22,6 +23,8 @@ public interface IStatement : IDisposable /// int ParameterCount { get; } + IEnumerable ColumnNames { get; } + /// /// Binds a value to the prepared statement. /// diff --git a/Libsql.Client/StatementWrapper.cs b/Libsql.Client/StatementWrapper.cs index e5988dc..8b26678 100644 --- a/Libsql.Client/StatementWrapper.cs +++ b/Libsql.Client/StatementWrapper.cs @@ -1,7 +1,10 @@ using System; +using System.Collections.Generic; using System.Diagnostics; +using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; +using Libsql.Client.Extensions; namespace Libsql.Client { @@ -10,6 +13,7 @@ internal class StatementWrapper : IStatement int IStatement.BoundValuesCount => _bindIndex - 1; public int ParameterCount => GetParameterCount(); + public IEnumerable ColumnNames => GetColumnNames(); private int _bindIndex = 1; public readonly libsql_stmt_t Stmt; @@ -129,11 +133,37 @@ private unsafe int GetParameterCount() int count; var exitCode = Bindings.libsql_stmt_parameter_count(Stmt, &count,&error.Ptr); - error.ThrowIfNonZero(exitCode, $"Failed to get parameter count"); + error.ThrowIfNonZero(exitCode, "Failed to get parameter count"); return count; } + private unsafe IEnumerable GetColumnNames() + { + var error = new Error(); + var ptr = (byte*)0; + int len; + + var exitCode = Bindings.libsql_stmt_columns(Stmt, &ptr, &len, &error.Ptr); + error.ThrowIfNonZero(exitCode, "Failed to get column names"); + Console.WriteLine((int)ptr); + var ptrs = new byte*[len]; + for (var i = 0; i < ptrs.Length; i++) + { + ptrs[i] = (byte*)Marshal.ReadByte((IntPtr)ptr, i); + } + + var names = new List(len); + + foreach (var namePtr in ptrs) + { + Console.WriteLine((int)namePtr); + // names.Add(CustomMarshal.PtrToStringUTF8((IntPtr)namePtr)); + } + + return names; + } + public void Bind(Integer integer, int index) { BindInt(integer.Value, index); @@ -186,6 +216,12 @@ public void Reset() private void ReleaseUnmanagedResources() { + var error = new Error(); + int exitCode; + unsafe { + exitCode = Bindings.libsql_stmt_finalize(Stmt, &error.Ptr); + } + error.ThrowIfNonZero(exitCode, "Failed to dispose of the statement"); Bindings.libsql_free_stmt(Stmt); } diff --git a/generate-bindings.sh b/generate-bindings.sh index 7feceaf..1e94dfd 100755 --- a/generate-bindings.sh +++ b/generate-bindings.sh @@ -41,11 +41,11 @@ fi # git sparse-checkout init # git sparse-checkout set bindings/c bindings/wasm libsql libsql-sys # ----- -# git fetch --quiet origin -# git checkout fb85262 +git fetch --quiet origin +git checkout e853d54 # ----- -git fetch --depth 1 origin tag libsql-rs-v${LIBSQL_RS_VERSION} -git reset --hard tags/libsql-rs-v${LIBSQL_RS_VERSION} +# git fetch --depth 1 origin tag libsql-rs-v${LIBSQL_RS_VERSION} +# git reset --hard tags/libsql-rs-v${LIBSQL_RS_VERSION} cd .. cargo build --release diff --git a/rust-bindings/Cargo.toml b/rust-bindings/Cargo.toml index 7b05dd4..25152b8 100644 --- a/rust-bindings/Cargo.toml +++ b/rust-bindings/Cargo.toml @@ -5,7 +5,7 @@ members = [ ] [workspace.dependencies] -rusqlite = { package = "libsql-rusqlite", path = "libsql/vendored/rusqlite", version = "0.32", default-features = false, features = [ +rusqlite = { package = "libsql-rusqlite", path = "libsql/vendored/rusqlite", version = "0.33", default-features = false, features = [ "libsql-experimental", "column_decltype", "load_extension", @@ -16,3 +16,4 @@ rusqlite = { package = "libsql-rusqlite", path = "libsql/vendored/rusqlite", ver ] } hyper = { version = "0.14" } tower = { version = "0.4.13" } +zerocopy = { version = "0.7.32", features = ["derive", "alloc"] } From 59ee312648e8b5c04e71737109c4c6091ba4c041 Mon Sep 17 00:00:00 2001 From: Tom van Dinther <39470469+tvandinther@users.noreply.github.com> Date: Sun, 31 Aug 2025 13:36:14 +0200 Subject: [PATCH 18/28] Remove column names --- Libsql.Client.Tests/StatementTests.cs | 13 ------------- Libsql.Client/IStatement.cs | 2 -- Libsql.Client/StatementWrapper.cs | 28 +-------------------------- 3 files changed, 1 insertion(+), 42 deletions(-) diff --git a/Libsql.Client.Tests/StatementTests.cs b/Libsql.Client.Tests/StatementTests.cs index 0c014c9..e50e1f1 100644 --- a/Libsql.Client.Tests/StatementTests.cs +++ b/Libsql.Client.Tests/StatementTests.cs @@ -189,17 +189,4 @@ public async Task Statement_CanBeReset() Assert.Equal(secondExpected, text2); } - - [Fact] - public async Task Statement_CanGet_ColumnNames() - { - using var statement = await _db.Prepare("SELECT 1 AS `one`, 2 AS `two`"); - - var columnNames = statement.ColumnNames; - - Assert.Collection(columnNames, - r => Assert.Equal("one", r), - r => Assert.Equal("two", r) - ); - } } \ No newline at end of file diff --git a/Libsql.Client/IStatement.cs b/Libsql.Client/IStatement.cs index 95cd39f..e01d94f 100644 --- a/Libsql.Client/IStatement.cs +++ b/Libsql.Client/IStatement.cs @@ -23,8 +23,6 @@ public interface IStatement : IDisposable /// int ParameterCount { get; } - IEnumerable ColumnNames { get; } - /// /// Binds a value to the prepared statement. /// diff --git a/Libsql.Client/StatementWrapper.cs b/Libsql.Client/StatementWrapper.cs index 8b26678..22e6c90 100644 --- a/Libsql.Client/StatementWrapper.cs +++ b/Libsql.Client/StatementWrapper.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Linq; using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; @@ -13,7 +14,6 @@ internal class StatementWrapper : IStatement int IStatement.BoundValuesCount => _bindIndex - 1; public int ParameterCount => GetParameterCount(); - public IEnumerable ColumnNames => GetColumnNames(); private int _bindIndex = 1; public readonly libsql_stmt_t Stmt; @@ -138,32 +138,6 @@ private unsafe int GetParameterCount() return count; } - private unsafe IEnumerable GetColumnNames() - { - var error = new Error(); - var ptr = (byte*)0; - int len; - - var exitCode = Bindings.libsql_stmt_columns(Stmt, &ptr, &len, &error.Ptr); - error.ThrowIfNonZero(exitCode, "Failed to get column names"); - Console.WriteLine((int)ptr); - var ptrs = new byte*[len]; - for (var i = 0; i < ptrs.Length; i++) - { - ptrs[i] = (byte*)Marshal.ReadByte((IntPtr)ptr, i); - } - - var names = new List(len); - - foreach (var namePtr in ptrs) - { - Console.WriteLine((int)namePtr); - // names.Add(CustomMarshal.PtrToStringUTF8((IntPtr)namePtr)); - } - - return names; - } - public void Bind(Integer integer, int index) { BindInt(integer.Value, index); From 9f078ae8b8c733d403bce0286f8e394fcba08882 Mon Sep 17 00:00:00 2001 From: Tom van Dinther <39470469+tvandinther@users.noreply.github.com> Date: Sun, 31 Aug 2025 13:45:45 +0200 Subject: [PATCH 19/28] Add devbox in favour of nix shell --- devbox.json | 19 ++++ devbox.lock | 245 ++++++++++++++++++++++++++++++++++++++++++++++++++++ shell.nix | 12 --- 3 files changed, 264 insertions(+), 12 deletions(-) create mode 100644 devbox.json create mode 100644 devbox.lock delete mode 100644 shell.nix diff --git a/devbox.json b/devbox.json new file mode 100644 index 0000000..25e2dfa --- /dev/null +++ b/devbox.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://raw.githubusercontent.com/jetify-com/devbox/0.14.0/.schema/devbox.schema.json", + "packages": [ + "cmake@latest", + "rustup@latest", + "just@latest", + "dotnet-sdk@8" + ], + "shell": { + "init_hook": [ + "rustup default stable" + ], + "scripts": { + "test": [ + "echo \"Error: no test specified\" && exit 1" + ] + } + } +} \ No newline at end of file diff --git a/devbox.lock b/devbox.lock new file mode 100644 index 0000000..e983bb3 --- /dev/null +++ b/devbox.lock @@ -0,0 +1,245 @@ +{ + "lockfile_version": "1", + "packages": { + "cmake@latest": { + "last_modified": "2025-07-28T17:09:23Z", + "resolved": "github:NixOS/nixpkgs/648f70160c03151bc2121d179291337ad6bc564b#cmake", + "source": "devbox-search", + "version": "3.31.7", + "systems": { + "aarch64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/77572xb49wl0728hvxsjwxsql1lxm30r-cmake-3.31.7", + "default": true + } + ], + "store_path": "/nix/store/77572xb49wl0728hvxsjwxsql1lxm30r-cmake-3.31.7" + }, + "aarch64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/2pbjx4gij8dvgs4cmh01dny1727jy5mi-cmake-3.31.7", + "default": true + }, + { + "name": "debug", + "path": "/nix/store/y23yv31zv0gn822cprm7njx28pdv3n6m-cmake-3.31.7-debug" + } + ], + "store_path": "/nix/store/2pbjx4gij8dvgs4cmh01dny1727jy5mi-cmake-3.31.7" + }, + "x86_64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/y50arqd9pz1f3cyr1rafp176wxig8ws5-cmake-3.31.7", + "default": true + } + ], + "store_path": "/nix/store/y50arqd9pz1f3cyr1rafp176wxig8ws5-cmake-3.31.7" + }, + "x86_64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/a3w3ka2d796ghlhf31il839c0658m5ih-cmake-3.31.7", + "default": true + }, + { + "name": "debug", + "path": "/nix/store/f8a07z4aiw19aywcvs70s30fbghq6i0d-cmake-3.31.7-debug" + } + ], + "store_path": "/nix/store/a3w3ka2d796ghlhf31il839c0658m5ih-cmake-3.31.7" + } + } + }, + "dotnet-sdk@8": { + "last_modified": "2025-08-10T04:38:50Z", + "resolved": "github:NixOS/nixpkgs/641d909c4a7538f1539da9240dedb1755c907e40#dotnet-sdk", + "source": "devbox-search", + "version": "8.0.413", + "systems": { + "aarch64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/zv4g7x2hw15qdq1i5gy21j5h37hgjpxy-dotnet-sdk-wrapped-8.0.413", + "default": true + } + ], + "store_path": "/nix/store/zv4g7x2hw15qdq1i5gy21j5h37hgjpxy-dotnet-sdk-wrapped-8.0.413" + }, + "aarch64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/c88hhi68g5l3q4s6n02z28l4an2gar0b-dotnet-sdk-wrapped-8.0.413", + "default": true + } + ], + "store_path": "/nix/store/c88hhi68g5l3q4s6n02z28l4an2gar0b-dotnet-sdk-wrapped-8.0.413" + }, + "x86_64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/rnmb0pzv4cvff0v1pm7ff2w6xklry4rm-dotnet-sdk-wrapped-8.0.413", + "default": true + } + ], + "store_path": "/nix/store/rnmb0pzv4cvff0v1pm7ff2w6xklry4rm-dotnet-sdk-wrapped-8.0.413" + }, + "x86_64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/g7gwwffr0v6m1wlg8faxy81k4cckm3hl-dotnet-sdk-wrapped-8.0.413", + "default": true + } + ], + "store_path": "/nix/store/g7gwwffr0v6m1wlg8faxy81k4cckm3hl-dotnet-sdk-wrapped-8.0.413" + } + } + }, + "github:NixOS/nixpkgs/nixpkgs-unstable": { + "resolved": "github:NixOS/nixpkgs/a918bb3594dd243c2f8534b3be01b3cb4ed35fd1?lastModified=1756536218&narHash=sha256-ynQxPVN2FIPheUgTFhv01gYLbaiSOS7NgWJPm9LF9D0%3D" + }, + "just@latest": { + "last_modified": "2025-07-28T17:09:23Z", + "resolved": "github:NixOS/nixpkgs/648f70160c03151bc2121d179291337ad6bc564b#just", + "source": "devbox-search", + "version": "1.42.4", + "systems": { + "aarch64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/rfqsf7a8dym7bxd8bjfhndnmx7gvbgph-just-1.42.4", + "default": true + }, + { + "name": "man", + "path": "/nix/store/gih21nbjpx4ip4d4l2h0g3hi6664k147-just-1.42.4-man", + "default": true + }, + { + "name": "doc", + "path": "/nix/store/pq9fksdkn6sbq3lli4z5y37085d5l43d-just-1.42.4-doc" + } + ], + "store_path": "/nix/store/rfqsf7a8dym7bxd8bjfhndnmx7gvbgph-just-1.42.4" + }, + "aarch64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/8xcqlacbwjfz518ca5hgjrf8208wjp8g-just-1.42.4", + "default": true + }, + { + "name": "man", + "path": "/nix/store/83901a2c4v4cgv1b2kx7l6m9wd174sp8-just-1.42.4-man", + "default": true + }, + { + "name": "doc", + "path": "/nix/store/z7vj1b56v3z7i64r20aa8y61ayqppqfp-just-1.42.4-doc" + } + ], + "store_path": "/nix/store/8xcqlacbwjfz518ca5hgjrf8208wjp8g-just-1.42.4" + }, + "x86_64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/xv86najr5k1r4fvg6yf8ha3m8b6bhdbj-just-1.42.4", + "default": true + }, + { + "name": "man", + "path": "/nix/store/1xj03nldjkkhgkih64iyakgfdj6pxwrx-just-1.42.4-man", + "default": true + }, + { + "name": "doc", + "path": "/nix/store/cbqy754s1xm921iw2gg97pmg9q6n7nd5-just-1.42.4-doc" + } + ], + "store_path": "/nix/store/xv86najr5k1r4fvg6yf8ha3m8b6bhdbj-just-1.42.4" + }, + "x86_64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/wnbn0xb6mzzbl594d51hqqvch1lnzhpb-just-1.42.4", + "default": true + }, + { + "name": "man", + "path": "/nix/store/bhry505vmc1rfpnr9yn1486488nsbpsg-just-1.42.4-man", + "default": true + }, + { + "name": "doc", + "path": "/nix/store/h5p6n1hg051hjs02jwlvbndlmx73nj45-just-1.42.4-doc" + } + ], + "store_path": "/nix/store/wnbn0xb6mzzbl594d51hqqvch1lnzhpb-just-1.42.4" + } + } + }, + "rustup@latest": { + "last_modified": "2025-07-28T17:09:23Z", + "plugin_version": "0.0.1", + "resolved": "github:NixOS/nixpkgs/648f70160c03151bc2121d179291337ad6bc564b#rustup", + "source": "devbox-search", + "version": "1.28.2", + "systems": { + "aarch64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/fhz4c3pawxqklssimadm9gp5jfagfl8v-rustup-1.28.2", + "default": true + } + ], + "store_path": "/nix/store/fhz4c3pawxqklssimadm9gp5jfagfl8v-rustup-1.28.2" + }, + "aarch64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/0z156naqlk96m681ns9jji0pw4vwqxq9-rustup-1.28.2", + "default": true + } + ], + "store_path": "/nix/store/0z156naqlk96m681ns9jji0pw4vwqxq9-rustup-1.28.2" + }, + "x86_64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/2zgwgadb3q97mcia0w2z643y17ifpjkz-rustup-1.28.2", + "default": true + } + ], + "store_path": "/nix/store/2zgwgadb3q97mcia0w2z643y17ifpjkz-rustup-1.28.2" + }, + "x86_64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/4z7n3dky3wmx7130rid6c67ra1ipqhn1-rustup-1.28.2", + "default": true + } + ], + "store_path": "/nix/store/4z7n3dky3wmx7130rid6c67ra1ipqhn1-rustup-1.28.2" + } + } + } + } +} diff --git a/shell.nix b/shell.nix deleted file mode 100644 index db38ef5..0000000 --- a/shell.nix +++ /dev/null @@ -1,12 +0,0 @@ -{ pkgs ? import {} }: - -pkgs.mkShell { - buildInputs = with pkgs; [ - rustup - cmake - ]; - - shellHook = '' - rustup default stable - ''; -} From ee9d84c97e37bcce3a22cb1bac2ede87b84bb264 Mon Sep 17 00:00:00 2001 From: Tom van Dinther <39470469+tvandinther@users.noreply.github.com> Date: Sun, 31 Aug 2025 13:46:41 +0200 Subject: [PATCH 20/28] Update libsql to 0.9.21 --- rust-bindings/Cargo.toml | 13 ++++++++++++- rust-bindings/csharp-bindings/Cargo.toml | 8 ++++++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/rust-bindings/Cargo.toml b/rust-bindings/Cargo.toml index 25152b8..60b68b7 100644 --- a/rust-bindings/Cargo.toml +++ b/rust-bindings/Cargo.toml @@ -4,8 +4,19 @@ members = [ "libsql/bindings/c" ] +[workspace.package] +edition = "2021" +authors = ["the libSQL authors"] +version = "0.9.21" +license = "MIT" +repository = "https://github.com/tursodatabase/libsql" + [workspace.dependencies] -rusqlite = { package = "libsql-rusqlite", path = "libsql/vendored/rusqlite", version = "0.33", default-features = false, features = [ +libsql-ffi = { path = "libsql/libsql-ffi", version = "0.9.21" } +libsql-sys = { path = "libsql/libsql-sys", version = "0.9.21", default-features = false } +libsql-hrana = { path = "libsql/libsql-hrana", version = "0.9.21" } +libsql_replication = { path = "libsql/libsql-replication", version = "0.9.21" } +rusqlite = { package = "libsql-rusqlite", path = "libsql/vendored/rusqlite", version = "0.9.21", default-features = false, features = [ "libsql-experimental", "column_decltype", "load_extension", diff --git a/rust-bindings/csharp-bindings/Cargo.toml b/rust-bindings/csharp-bindings/Cargo.toml index f66531d..3ba8922 100644 --- a/rust-bindings/csharp-bindings/Cargo.toml +++ b/rust-bindings/csharp-bindings/Cargo.toml @@ -9,11 +9,15 @@ path = "../libsql/bindings/c/src/lib.rs" doc = false [build-dependencies] -csbindgen = "1.2.0" +csbindgen = "1.9.5" [dependencies] bytes = "1.5.0" -libsql = { path = "../libsql/libsql" } lazy_static = "1.4.0" tokio = { version = "1.29.1", features = [ "rt-multi-thread" ] } hyper-rustls = { version = "0.25", features = ["webpki-roots"]} +tracing = "0.1.40" +tracing-subscriber = "0.3.18" +http = "1.1.0" +anyhow = "1.0.86" +libsql = { path = "../libsql/libsql", features = ["encryption"] } From 79c534b1c9a91cb61d1cc991c5d58e576c5554d3 Mon Sep 17 00:00:00 2001 From: Tom van Dinther <39470469+tvandinther@users.noreply.github.com> Date: Sun, 31 Aug 2025 14:04:21 +0200 Subject: [PATCH 21/28] update script to allow for more options --- generate-bindings.sh | 53 ++++++++++++++++++++++++++++------------ rust-bindings/.gitignore | 2 +- 2 files changed, 39 insertions(+), 16 deletions(-) diff --git a/generate-bindings.sh b/generate-bindings.sh index 1e94dfd..28784e8 100755 --- a/generate-bindings.sh +++ b/generate-bindings.sh @@ -6,6 +6,9 @@ script_dir="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" export LIBSQL_RS_VERSION="0.5.0" bindings_dir="${script_dir}/rust-bindings" force=false +commit="${LIBSQL_COMMIT:-libsql-0.9.21}" +remote="${LIBSQL_REMOTE:-https://github.com/tursodatabase/libsql.git}" +local_repo_path="" # Parse command line arguments while [[ $# -gt 0 ]]; do @@ -14,6 +17,18 @@ while [[ $# -gt 0 ]]; do force=true shift ;; + --commit) + commit="$2" + shift 2 + ;; + --remote) + remote="$2" + shift 2 + ;; + --local) + local_repo_path="$2" + shift 2 + ;; *) echo "Unknown option: $1" exit 1 @@ -29,23 +44,31 @@ function run_if_force { run_if_force rm -rf "${bindings_dir}/libsql" run_if_force rm -rf "${bindings_dir}/target" -mkdir -p "${bindings_dir}/libsql" +mkdir -p "${bindings_dir}" -cd "${bindings_dir}/libsql" -git init 2> /dev/null +if [[ -n "${local_repo_path}" ]]; then + # Use local repository: create symlink + ln -sfn "${local_repo_path}" "${bindings_dir}/libsql" +else + # Use remote repository: clone/fetch and checkout + mkdir -p "${bindings_dir}/libsql" + cd "${bindings_dir}/libsql" + git init 2> /dev/null -if ! git config remote.origin.url &> /dev/null;then - git remote add --no-fetch origin "https://github.com/tursodatabase/libsql.git" -fi + if ! git config remote.origin.url &> /dev/null;then + git remote add --no-fetch origin "${remote}" + # git remote add --no-fetch origin "https://github.com/tvandinther/libsql.git" + fi -# git sparse-checkout init -# git sparse-checkout set bindings/c bindings/wasm libsql libsql-sys -# ----- -git fetch --quiet origin -git checkout e853d54 -# ----- -# git fetch --depth 1 origin tag libsql-rs-v${LIBSQL_RS_VERSION} -# git reset --hard tags/libsql-rs-v${LIBSQL_RS_VERSION} + git fetch --quiet origin + # git checkout e853d54 + # git checkout 204f45d + git checkout "${commit}" + # ----- + # git fetch --depth 1 origin tag libsql-rs-v${LIBSQL_RS_VERSION} + # git reset --hard tags/libsql-rs-v${LIBSQL_RS_VERSION} + cd ../.. +fi -cd .. +cd "${bindings_dir}" cargo build --release diff --git a/rust-bindings/.gitignore b/rust-bindings/.gitignore index ca34ade..00bb527 100644 --- a/rust-bindings/.gitignore +++ b/rust-bindings/.gitignore @@ -1,4 +1,4 @@ -libsql/ +libsql target/ bindings/ Cargo.lock \ No newline at end of file From 8900adeb753f8d80ffca52a14cf24447d321f388 Mon Sep 17 00:00:00 2001 From: Tom van Dinther <39470469+tvandinther@users.noreply.github.com> Date: Sun, 31 Aug 2025 14:11:51 +0200 Subject: [PATCH 22/28] Add just --- .github/workflows/build-dependencies.yaml | 7 ++- devbox.json | 5 +- devbox.lock | 68 +++++++++++++++++++++++ justfile | 5 ++ 4 files changed, 82 insertions(+), 3 deletions(-) create mode 100644 justfile diff --git a/.github/workflows/build-dependencies.yaml b/.github/workflows/build-dependencies.yaml index 00539a4..480c028 100644 --- a/.github/workflows/build-dependencies.yaml +++ b/.github/workflows/build-dependencies.yaml @@ -27,10 +27,15 @@ jobs: restore-keys: | ${{ runner.os }}-rust-bindings- + - name: Install devbox + uses: jetify-com/devbox-install-action@v0.13.0 + with: + enable-cache: 'true' + - name: Generate bindings if: | steps.cache-rust-bindings.outputs.cache-hit != 'true' - run: ./generate-bindings.sh + run: devbox run -- just generate-bindings --remote https://github.com/tvandinther/libsql --commit add-missing-statement-functions shell: bash - name: Upload bindings diff --git a/devbox.json b/devbox.json index 25e2dfa..74df18f 100644 --- a/devbox.json +++ b/devbox.json @@ -4,7 +4,8 @@ "cmake@latest", "rustup@latest", "just@latest", - "dotnet-sdk@8" + "dotnet-sdk@8", + "fzf@latest" ], "shell": { "init_hook": [ @@ -16,4 +17,4 @@ ] } } -} \ No newline at end of file +} diff --git a/devbox.lock b/devbox.lock index e983bb3..c2effab 100644 --- a/devbox.lock +++ b/devbox.lock @@ -105,6 +105,74 @@ } } }, + "fzf@latest": { + "last_modified": "2025-08-08T08:05:48Z", + "resolved": "github:NixOS/nixpkgs/a3f3e3f2c983e957af6b07a1db98bafd1f87b7a1#fzf", + "source": "devbox-search", + "version": "0.65.1", + "systems": { + "aarch64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/1g47v585sy9mbp83bpr15xm00jk6zfqd-fzf-0.65.1", + "default": true + }, + { + "name": "man", + "path": "/nix/store/bi6hvv72zh2zqgagirqvpjddpyfb8jkq-fzf-0.65.1-man", + "default": true + } + ], + "store_path": "/nix/store/1g47v585sy9mbp83bpr15xm00jk6zfqd-fzf-0.65.1" + }, + "aarch64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/cxw0r42cdwggxgnsxzh5v7h5szb4zn52-fzf-0.65.1", + "default": true + }, + { + "name": "man", + "path": "/nix/store/rw1fi4rdjpxcv1x1ln5q5iqdgp3bi0k3-fzf-0.65.1-man", + "default": true + } + ], + "store_path": "/nix/store/cxw0r42cdwggxgnsxzh5v7h5szb4zn52-fzf-0.65.1" + }, + "x86_64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/dnjnbx0yci902phzbdcjc5d6328xswy6-fzf-0.65.1", + "default": true + }, + { + "name": "man", + "path": "/nix/store/1f9mh3lghzdj06wf5g4cdypyva712idf-fzf-0.65.1-man", + "default": true + } + ], + "store_path": "/nix/store/dnjnbx0yci902phzbdcjc5d6328xswy6-fzf-0.65.1" + }, + "x86_64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/6a4wknif1z62kdn3cbkak78kyf462cxj-fzf-0.65.1", + "default": true + }, + { + "name": "man", + "path": "/nix/store/2jb1xjfcilpzcgd692qb84gn87cvvhl7-fzf-0.65.1-man", + "default": true + } + ], + "store_path": "/nix/store/6a4wknif1z62kdn3cbkak78kyf462cxj-fzf-0.65.1" + } + } + }, "github:NixOS/nixpkgs/nixpkgs-unstable": { "resolved": "github:NixOS/nixpkgs/a918bb3594dd243c2f8534b3be01b3cb4ed35fd1?lastModified=1756536218&narHash=sha256-ynQxPVN2FIPheUgTFhv01gYLbaiSOS7NgWJPm9LF9D0%3D" }, diff --git a/justfile b/justfile new file mode 100644 index 0000000..33193e3 --- /dev/null +++ b/justfile @@ -0,0 +1,5 @@ +default: + @just --choose + +generate-bindings *OPTIONS: + ./generate-bindings.sh {{OPTIONS}} From 2b3c305289eb69fd942fe7cfa026290693620769 Mon Sep 17 00:00:00 2001 From: Tom van Dinther <39470469+tvandinther@users.noreply.github.com> Date: Sun, 31 Aug 2025 14:11:59 +0200 Subject: [PATCH 23/28] add workspace extension recommendations --- .vscode/extensions.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .vscode/extensions.json diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..ea629f2 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,7 @@ +{ + "recommendations": [ + "nefrob.vscode-just-syntax", + "ms-dotnettools.csharp", + "rust-lang.rust-analyzer" + ] +} \ No newline at end of file From 848afd01170f515976fc223061f69914e5f1f398 Mon Sep 17 00:00:00 2001 From: Tom van Dinther <39470469+tvandinther@users.noreply.github.com> Date: Sun, 31 Aug 2025 14:14:47 +0200 Subject: [PATCH 24/28] Remove devbox from CI --- .github/workflows/build-dependencies.yaml | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/.github/workflows/build-dependencies.yaml b/.github/workflows/build-dependencies.yaml index 480c028..dec5e8e 100644 --- a/.github/workflows/build-dependencies.yaml +++ b/.github/workflows/build-dependencies.yaml @@ -27,15 +27,10 @@ jobs: restore-keys: | ${{ runner.os }}-rust-bindings- - - name: Install devbox - uses: jetify-com/devbox-install-action@v0.13.0 - with: - enable-cache: 'true' - - name: Generate bindings if: | steps.cache-rust-bindings.outputs.cache-hit != 'true' - run: devbox run -- just generate-bindings --remote https://github.com/tvandinther/libsql --commit add-missing-statement-functions + run: ./generate-bindings.sh --remote https://github.com/tvandinther/libsql --commit add-missing-statement-functions shell: bash - name: Upload bindings From 65c2a801489c1b6a7c8d2de8043359459e1a1bb8 Mon Sep 17 00:00:00 2001 From: Tom van Dinther <39470469+tvandinther@users.noreply.github.com> Date: Sun, 31 Aug 2025 14:19:43 +0200 Subject: [PATCH 25/28] Update README to include checkmark on prepared statements --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b056998..506f44e 100644 --- a/README.md +++ b/README.md @@ -188,7 +188,7 @@ The full test suite is run only on a Linux x64 platform. Most of the test suite - [x] Non-parameterised. - [x] Parameterised with positional arguments. - [ ] Parameterised with named arguments. -- [ ] Prepared statements. +- [x] Prepared statements. - [ ] Batched statements. - [ ] Transactions. - [x] A result set is returned from an execution. From f318d3d25f555a19356dff0ba95f929eae9fa075 Mon Sep 17 00:00:00 2001 From: Tom van Dinther <39470469+tvandinther@users.noreply.github.com> Date: Sun, 31 Aug 2025 14:25:17 +0200 Subject: [PATCH 26/28] Update Demo to use new syntax --- Demo/Program.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Demo/Program.cs b/Demo/Program.cs index 616344a..b0a27b3 100644 --- a/Demo/Program.cs +++ b/Demo/Program.cs @@ -7,20 +7,20 @@ // Execute SQL statements directly -var rs = await dbClient.Execute("CREATE TABLE IF NOT EXISTS `users` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT NOT NULL, `height` REAL, `data` BLOB)"); +var rs = await dbClient.Query("CREATE TABLE IF NOT EXISTS `users` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT NOT NULL, `height` REAL, `data` BLOB)"); // Read the results by using the IResultSet interface -var rs1 = await dbClient.Execute("INSERT INTO `users` (`name`, `height`, `data`) VALUES ('John Doe', 182.6, X'a4c7b8e21d3f50a6b9d2e8f7c1349a0b5c6d7e218349b6d012c71e8f9a093fed'), ('Jane Doe', 0.5, X'00')"); +var rs1 = await dbClient.Query("INSERT INTO `users` (`name`, `height`, `data`) VALUES ('John Doe', 182.6, X'a4c7b8e21d3f50a6b9d2e8f7c1349a0b5c6d7e218349b6d012c71e8f9a093fed'), ('Jane Doe', 0.5, X'00')"); Console.WriteLine($"Inserted {rs1.RowsAffected} rows"); Console.WriteLine($"Last inserted id: {rs1.LastInsertRowId}"); -var rs2 = await dbClient.Execute("SELECT `id`, `name`, `height`, `data` FROM `users`"); +var rs2 = await dbClient.Query("SELECT `id`, `name`, `height`, `data` FROM `users`"); PrintTable(rs2); // Using positional arguments var searchString = "hn"; -var rs3 = await dbClient.Execute("SELECT `id`, `name`, `height`, `data` FROM `users` WHERE `name` LIKE concat('%', ?, '%')", searchString); +var rs3 = await dbClient.Query("SELECT `id`, `name`, `height`, `data` FROM `users` WHERE `name` LIKE concat('%', ?, '%')", searchString); PrintTable(rs3); From 1d5e778921ea89e7302dc89c43d09242a5d08e0d Mon Sep 17 00:00:00 2001 From: Tom van Dinther <39470469+tvandinther@users.noreply.github.com> Date: Sun, 31 Aug 2025 14:27:11 +0200 Subject: [PATCH 27/28] Simplify script --- generate-bindings.sh | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/generate-bindings.sh b/generate-bindings.sh index 28784e8..601daa6 100755 --- a/generate-bindings.sh +++ b/generate-bindings.sh @@ -3,14 +3,12 @@ set -euo pipefail script_dir="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" -export LIBSQL_RS_VERSION="0.5.0" bindings_dir="${script_dir}/rust-bindings" force=false commit="${LIBSQL_COMMIT:-libsql-0.9.21}" remote="${LIBSQL_REMOTE:-https://github.com/tursodatabase/libsql.git}" local_repo_path="" -# Parse command line arguments while [[ $# -gt 0 ]]; do case "$1" in --force) @@ -47,26 +45,18 @@ run_if_force rm -rf "${bindings_dir}/target" mkdir -p "${bindings_dir}" if [[ -n "${local_repo_path}" ]]; then - # Use local repository: create symlink ln -sfn "${local_repo_path}" "${bindings_dir}/libsql" else - # Use remote repository: clone/fetch and checkout mkdir -p "${bindings_dir}/libsql" cd "${bindings_dir}/libsql" git init 2> /dev/null if ! git config remote.origin.url &> /dev/null;then git remote add --no-fetch origin "${remote}" - # git remote add --no-fetch origin "https://github.com/tvandinther/libsql.git" fi git fetch --quiet origin - # git checkout e853d54 - # git checkout 204f45d git checkout "${commit}" - # ----- - # git fetch --depth 1 origin tag libsql-rs-v${LIBSQL_RS_VERSION} - # git reset --hard tags/libsql-rs-v${LIBSQL_RS_VERSION} cd ../.. fi From d9f767cd7f22e1c56fb0c78e2a246bbf5b596f86 Mon Sep 17 00:00:00 2001 From: Tom van Dinther <39470469+tvandinther@users.noreply.github.com> Date: Sun, 31 Aug 2025 15:31:13 +0200 Subject: [PATCH 28/28] WIP --- Libsql.Client/Rows.cs | 1 + generate-bindings.sh | 2 +- justfile | 6 ++++++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/Libsql.Client/Rows.cs b/Libsql.Client/Rows.cs index 8bcae46..d712fe4 100644 --- a/Libsql.Client/Rows.cs +++ b/Libsql.Client/Rows.cs @@ -65,6 +65,7 @@ private unsafe void PopulateColumnTypes(int columnCount) { int columnType; var error = new Error(); Debug.Assert(firstRow.ptr != null, "firstRow is null. Can not find column type on a null pointer."); + // TODO: Investigate why this call crashes tests in PositionalArgumentTests.cs var errorCode = Bindings.libsql_column_type(_libsqlRowsT, firstRow, i, &columnType, &error.Ptr); error.ThrowIfNonZero(errorCode, "Failed to get column type"); _enumeratorData.ColumnTypes[i] = (ValueType)columnType; diff --git a/generate-bindings.sh b/generate-bindings.sh index 601daa6..20a2a6f 100755 --- a/generate-bindings.sh +++ b/generate-bindings.sh @@ -61,4 +61,4 @@ else fi cd "${bindings_dir}" -cargo build --release +cargo build --lib #--release diff --git a/justfile b/justfile index 33193e3..6415369 100644 --- a/justfile +++ b/justfile @@ -3,3 +3,9 @@ default: generate-bindings *OPTIONS: ./generate-bindings.sh {{OPTIONS}} + +test: + dotnet test + +test-file *FILE: + dotnet test --filter "FullyQualifiedName~Libsql.Client.Tests.{{FILE}}"