diff --git a/.github/workflows/build-dependencies.yaml b/.github/workflows/build-dependencies.yaml index 00539a4..dec5e8e 100644 --- a/.github/workflows/build-dependencies.yaml +++ b/.github/workflows/build-dependencies.yaml @@ -30,7 +30,7 @@ jobs: - name: Generate bindings if: | steps.cache-rust-bindings.outputs.cache-hit != 'true' - run: ./generate-bindings.sh + run: ./generate-bindings.sh --remote https://github.com/tvandinther/libsql --commit add-missing-statement-functions shell: bash - name: Upload bindings 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 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); 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..d54fac7 --- /dev/null +++ b/Libsql.Client.Tests/ExecuteTests.cs @@ -0,0 +1,64 @@ +namespace Libsql.Client.Tests; + +public class ExecuteTests +{ + private readonly IDatabaseClient _db = DatabaseClient.Create().Result; + + [Fact] + public async Task CreateTable_NoRowsAffected() + { + var rowsAffected = await _db.Execute("CREATE TABLE `test` (`id` INTEGER PRIMARY KEY AUTOINCREMENT)"); + + Assert.Equal(0ul, rowsAffected); + } + + + + [Fact] + public async Task RowsAffected_ReturnsOne_WhenRepeatedInserts() + { + await _db.Query("CREATE TABLE `test` (`id` INTEGER PRIMARY KEY AUTOINCREMENT)"); + + for (int i = 0; i < 10; i++) + { + var rowsAffected = await _db.Execute("INSERT INTO `test` DEFAULT VALUES"); + Assert.Equal(1ul, rowsAffected); + } + } + + [Fact] + public async Task RowsAffected_ReturnsExpectedValue() + { + await _db.Query("CREATE TABLE `test` (`id` INTEGER PRIMARY KEY AUTOINCREMENT)"); + + var rs = await _db.Query("INSERT INTO `test` DEFAULT VALUES"); + + Assert.Equal(1ul, rs.RowsAffected); + } + + [Fact] + public async Task RowsAffected_ReturnsExectedValue_WhenMultipleUpdates() + { + await _db.Execute("CREATE TABLE `test` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `value` INTEGER)"); + + for (int i = 0; i < 10; i++) + { + var rs = await _db.Execute("INSERT INTO `test` DEFAULT VALUES"); + } + + var rs2 = await _db.Query("UPDATE `test` SET `value` = 1"); + + Assert.Equal(10ul, rs2.RowsAffected); + } + + [Fact] + public async Task Throws_WhenRowsReturned() + { + async Task action() + { + await _db.Execute("SELECT 1"); + } + + await Assert.ThrowsAsync(action); + } +} diff --git a/Libsql.Client.Tests/PositionalArgumentTests.cs b/Libsql.Client.Tests/PositionalArgumentTests.cs index 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..b1dcb80 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()); @@ -43,12 +43,20 @@ public async Task Columns_NamesAreMarshalled_WhenSeveral() Assert.Equal("column_two", columns.Last()); } + [Fact] + public async Task LastInsertRowId_Zero_WhenNonQuery() + { + var rs = await _db.Query("CREATE TABLE `test` (`id` INTEGER PRIMARY KEY AUTOINCREMENT)"); + + Assert.Equal(0, rs.LastInsertRowId); + } + [Fact] public async Task LastInsertRowId_ReturnsExpectedValue() { await _db.Execute("CREATE TABLE `test` (`id` INTEGER PRIMARY KEY AUTOINCREMENT)"); - var rs = await _db.Execute("INSERT INTO `test` DEFAULT VALUES"); + var rs = await _db.Query("INSERT INTO `test` DEFAULT VALUES"); Assert.Equal(1, rs.LastInsertRowId); } @@ -61,23 +69,31 @@ public async Task LastInsertRowId_ReturnsExpectedValue_WhenMultipleInserts() IResultSet rs; for (int i = 0; i < 10; i++) { - rs = await _db.Execute("INSERT INTO `test` DEFAULT VALUES"); + rs = await _db.Query("INSERT INTO `test` DEFAULT VALUES"); Assert.Equal(i + 1, rs.LastInsertRowId); } } [Fact] - public async Task Changes_ReturnsExpectedValue() + public async Task RowsAffected_Zero_WhenNonQuery() + { + var rs = await _db.Query("CREATE TABLE `test` (`id` INTEGER PRIMARY KEY AUTOINCREMENT)"); + + Assert.Equal(0ul, rs.RowsAffected); + } + + [Fact] + public async Task RowsAffected_ReturnsExpectedValue() { await _db.Execute("CREATE TABLE `test` (`id` INTEGER PRIMARY KEY AUTOINCREMENT)"); - var rs = await _db.Execute("INSERT INTO `test` DEFAULT VALUES"); + var rs = await _db.Query("INSERT INTO `test` DEFAULT VALUES"); Assert.Equal(1ul, rs.RowsAffected); } [Fact] - public async Task Changes_ReturnsExectedValue_WhenMultipleUpdates() + public async Task RowsAffected_ReturnsExectedValue_WhenMultipleUpdates() { await _db.Execute("CREATE TABLE `test` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `value` INTEGER)"); @@ -86,7 +102,7 @@ public async Task Changes_ReturnsExectedValue_WhenMultipleUpdates() var rs = await _db.Execute("INSERT INTO `test` DEFAULT VALUES"); } - var rs2 = await _db.Execute("UPDATE `test` SET `value` = 1"); + var rs2 = await _db.Query("UPDATE `test` SET `value` = 1"); Assert.Equal(10ul, rs2.RowsAffected); } diff --git a/Libsql.Client.Tests/RowsTests.cs b/Libsql.Client.Tests/RowsTests.cs index 0ac54f3..867c550 100644 --- a/Libsql.Client.Tests/RowsTests.cs +++ b/Libsql.Client.Tests/RowsTests.cs @@ -3,11 +3,53 @@ 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() { - var rs = await _db.Execute("SELECT 1 WHERE 1 = 0"); + var rs = await _db.Query("SELECT 1 WHERE 1 = 0"); var rows = rs.Rows; Assert.Empty(rows); @@ -16,7 +58,7 @@ public async Task Rows_WhenEmpty() [Fact] public async Task Rows_CanIterateTwice() { - var rs = await _db.Execute("SELECT 1"); + var rs = await _db.Query("SELECT 1"); var firstArray = rs.Rows.ToArray(); var secondArray = rs.Rows.ToArray(); @@ -27,7 +69,7 @@ public async Task Rows_CanIterateTwice() [Fact] public async Task Rows_CanPartiallyIterateTwice() { - var rs = await _db.Execute("SELECT 1 UNION SELECT 2 UNION SELECT 3"); + var rs = await _db.Query("SELECT 1 UNION SELECT 2 UNION SELECT 3"); var firstArray = rs.Rows.Take(2).ToArray(); var secondArray = rs.Rows.ToArray(); diff --git a/Libsql.Client.Tests/SelectTests.cs b/Libsql.Client.Tests/SelectTests.cs index 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.Tests/StatementTests.cs b/Libsql.Client.Tests/StatementTests.cs new file mode 100644 index 0000000..e50e1f1 --- /dev/null +++ b/Libsql.Client.Tests/StatementTests.cs @@ -0,0 +1,192 @@ +namespace Libsql.Client.Tests; + +public class StatementTests +{ + private readonly IDatabaseClient _db = DatabaseClient.Create().Result; + + [Fact] + public async Task Statement_CanBind_Integer() + { + using var statement = await _db.Prepare("SELECT ?"); + var expected = 1; + + statement.Bind(new Integer(expected), 1); + var rs = await statement.Query(); + var row = rs.Rows.First(); + var value = row.First(); + var integer = Assert.IsType(value); + + Assert.Equal(expected, integer); + } + + [Fact] + public async Task Statement_CanBind_Real() + { + using var statement = await _db.Prepare("SELECT ?"); + var expected = 0.5; + + statement.Bind(expected, 1); + var rs = await statement.Query(); + var row = rs.Rows.First(); + var value = row.First(); + var integer = Assert.IsType(value); + + Assert.Equal(expected, integer); + } + + [Fact] + public async Task Statement_CanBind_Text() + { + using var statement = await _db.Prepare("SELECT ?"); + var expected = "hello"; + + statement.Bind(expected, 1); + var rs = await statement.Query(); + var row = rs.Rows.First(); + var value = row.First(); + var text = Assert.IsType(value); + + Assert.Equal(expected, text); + } + + [Fact] + public async Task Statement_CanBind_Blob() + { + using var statement = await _db.Prepare("SELECT ?"); + var expected = new byte[]{30, 9, 42, 76}; + + statement.Bind(expected, 1); + var rs = await statement.Query(); + var row = rs.Rows.First(); + var value = row.First(); + var blob = Assert.IsType(value); + + Assert.Equal(expected, blob); + } + + [Fact] + public async Task Statement_CanBind_Null() + { + using var statement = await _db.Prepare("SELECT ?"); + + statement.BindNull(1); + var rs = await statement.Query(); + var row = rs.Rows.First(); + var value = row.First(); + + Assert.IsType(value); + } + + [Fact] + public async Task Statement_CanBind_NNNIndex() + { + using var statement = await _db.Prepare("SELECT ?123"); + var expected = "Parameter annotated as the 123rd position"; + + statement.Bind(expected, 123); + var rs = await statement.Query(); + var row = rs.Rows.First(); + var value = row.First(); + + var text = Assert.IsType(value); + Assert.Equal(expected, text.Value); + } + + [Theory] + [InlineData("SELECT 1", 0)] + [InlineData("SELECT ?", 1)] + [InlineData("SELECT ?, ?", 2)] + [InlineData("SELECT ?, ?, ?", 3)] + [InlineData("SELECT ?10, ?11, ?33", 33)] + [InlineData("SELECT ?10, ?33, ?11", 33)] + [InlineData("SELECT ?, ?, ?2", 2)] + [InlineData("SELECT ?, ?, ?2, ?, ?", 4)] + public async Task Statement_CanGetParameterCount(string query, int expected) + { + using var statement = await _db.Prepare(query); + + var parameterCount = statement.ParameterCount; + + Assert.Equal(expected, parameterCount); + } + + [Fact] + public async Task Statement_BoundValuesCount_IsCorrect() + { + using var statement = await _db.Prepare("SELECT ?, ?, ?"); + statement.Bind("One", 1); + statement.Bind("Two", 2); + statement.Bind("Three", 3); + + var numberOfBoundValues = statement.BoundValuesCount; + + Assert.Equal(3, numberOfBoundValues); + } + + [Fact] + public async Task Database_CanQuery_Statements() + { + using var statement = await _db.Prepare("SELECT ?"); + var expected = 1; + + statement.Bind(new Integer(expected), 1); + var rs = await _db.Query(statement); + var row = rs.Rows.First(); + var value = row.First(); + var integer = Assert.IsType(value); + + Assert.Equal(expected, integer); + } + + [Fact] + public async Task Statement_CanExecute() + { + await _db.Execute("CREATE TABLE `test` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT)"); + using var statement = await _db.Prepare("INSERT INTO `test` (`name`) VALUES ('a'), ('b'), ('c')"); + + var rowsAffected = await statement.Execute(); + + Assert.Equal(3ul, rowsAffected); + } + + + [Fact] + public async Task Database_CanExecute_Statements() + { + await _db.Execute("CREATE TABLE `test` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT)"); + using var statement = await _db.Prepare("INSERT INTO `test` (`name`) VALUES ('a'), ('b'), ('c')"); + + var rowsAffected = await _db.Execute(statement); + + Assert.Equal(3ul, rowsAffected); + } + + [Fact] + public async Task Statement_CanBeReset() + { + await _db.Execute("CREATE TABLE `test` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT)"); + await _db.Execute("INSERT INTO `test` (`name`) VALUES ('a'), ('b'), ('c')"); + + using var statement = await _db.Prepare("SELECT `name` FROM `test` WHERE `id` = ?"); + var firstExpected = "a"; + var secondExpected = "b"; + + statement.Bind(new Integer(1), 1); + var rs = await statement.Query(); + var row = rs.Rows.First(); + var value = row.First(); + var text = Assert.IsType(value); + + Assert.Equal(firstExpected, text); + + statement.Reset(); + + statement.Bind(new Integer(2), 1); + var rs2 = await statement.Query(); + var row2 = rs2.Rows.First(); + var value2 = row2.First(); + var text2 = Assert.IsType(value2); + + Assert.Equal(secondExpected, text2); + } +} \ No newline at end of file diff --git a/Libsql.Client/DatabaseWrapper.cs b/Libsql.Client/DatabaseWrapper.cs index 68bb3d8..cd782c0 100644 --- a/Libsql.Client/DatabaseWrapper.cs +++ b/Libsql.Client/DatabaseWrapper.cs @@ -139,35 +139,85 @@ internal unsafe void Connect() error.ThrowIfNonZero(exitCode, "Failed to connect to database"); } + + public Task Query(string sql) + { + return Task.Run(() => + { + using (var statement = new StatementWrapper(this, _connection, sql)) + { + return QueryStatement(statement); + } + }); + } + + public Task Query(string sql, params object[] args) + { + return Task.Run(() => { + using (var statement = new StatementWrapper(this, _connection, sql)) + { + statement.BindAll(args); + + return QueryStatement(statement); + } + }); + } - public async Task Execute(string sql) + public Task Execute(string sql) { - return await Task.Run(() => + return Task.Run(() => { - var statement = new Statement(_connection, sql); - return ExecuteStatement(statement); + using (var statement = new StatementWrapper(this, _connection, sql)) + { + return ExecuteStatement(statement); + }; }); } - public async Task Execute(string sql, params object[] args) + public Task Execute(string sql, params object[] args) { - return await Task.Run(() => { - var statement = new Statement(_connection, sql); - statement.Bind(args); + return Task.Run(() => { + using (var statement = new StatementWrapper(this, _connection, sql)) + { + statement.BindAll(args); + return ExecuteStatement(statement); + } + }); + } + + public Task Query(IStatement statement) + { + return statement.Query(); + } + + public Task Execute(IStatement statement) + { + return statement.Execute(); + } + + internal Task Query(StatementWrapper statement) + { + return Task.Run(() => { + return QueryStatement(statement); + }); + } + + internal Task Execute(StatementWrapper statement) + { + return Task.Run(() => { return ExecuteStatement(statement); }); } - // TODO: Differentiate query statements and execute statements. - private unsafe IResultSet ExecuteStatement(Statement statement) + + private unsafe IResultSet QueryStatement(StatementWrapper statement) { var error = new Error(); var rows = new libsql_rows_t(); int exitCode; exitCode = Bindings.libsql_query_stmt(statement.Stmt, &rows, &error.Ptr); - statement.Dispose(); error.ThrowIfNonZero(exitCode, "Failed to execute statement"); @@ -179,14 +229,26 @@ private unsafe IResultSet ExecuteStatement(Statement statement) ); } - public async Task Sync() + private unsafe ulong ExecuteStatement(StatementWrapper statement) + { + var error = new Error(); + int exitCode; + + exitCode = Bindings.libsql_execute_stmt(statement.Stmt, &error.Ptr); + + error.ThrowIfNonZero(exitCode, "Failed to execute statement"); + + return Bindings.libsql_changes(_connection); + } + + public Task Sync() { if (_type != DatabaseType.EmbeddedReplica) { throw new InvalidOperationException("Cannot sync a non-replica database"); } - await Task.Run(() => + return Task.Run(() => { unsafe { @@ -200,6 +262,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 3285d55..8daf5ab 100644 --- a/Libsql.Client/IDatabaseClient.cs +++ b/Libsql.Client/IDatabaseClient.cs @@ -14,7 +14,8 @@ 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); + /// Use if your prepared statement does not return rows. + Task Query(string sql); /// /// Executes the given SQL query with the specified parameters and returns the result set. @@ -23,7 +24,52 @@ 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); + /// Use if your prepared statement does not return rows. + Task Query(string sql, params object[] args); + + /// + /// Executes the given prepared statement and returns the result set. + /// + /// The prepared statement to execute. + /// The result set returned by the prepared statement. + /// Thrown when the prepared statement fails to execute. + /// Use if your prepared statement does not return rows. + Task Query(IStatement statement); + + /// + /// Executes the given SQL. + /// + /// The SQL query to execute. + /// The number of affected rows. + /// Thrown when the SQL fails to execute. + /// Use if your prepared statement returns rows. + Task Execute(string sql); + + /// + /// Executes the given SQL with the specified parameters. + /// + /// The SQL query to execute. + /// The parameters to use in the query. + /// The number of affected rows. + /// Thrown when the SQL fails to execute. + /// Use if your prepared statement returns rows. + Task Execute(string sql, params object[] args); + + /// + /// Executes the given prepared statement. + /// + /// The prepared statement to execute. + /// The number of affected rows. + /// Thrown when the prepared statement fails to execute. + /// Use if your prepared statement returns rows. + Task Execute(IStatement statement); + + /// + /// Prepares a statement with the database ready for values to be bound to it. + /// + /// The SQL statement to prepare. + /// A statement to bind values to. + Task Prepare(string sql); /// /// Synchronises the embedded replica database with the remote database. diff --git a/Libsql.Client/IStatement.cs b/Libsql.Client/IStatement.cs new file mode 100644 index 0000000..e01d94f --- /dev/null +++ b/Libsql.Client/IStatement.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Libsql.Client +{ + /// + /// A prepared statement. + /// + public interface IStatement : IDisposable + { + /// + /// Gets the number of values currently bound on the prepared statement. + /// + int BoundValuesCount { get; } + + /// + /// Gets the number of parameters in the prepared statement. + /// + /// + /// If ?NNN syntax is used, the Parameter count is the maximum value of + /// NNN used plus any unannotated positional parameters to the right. + /// + int ParameterCount { get; } + + /// + /// Binds a value to the prepared statement. + /// + /// The value to bind. + /// The positional index to bind the value to. + /// The leftmost parameter has an index of 1. + /// If ?NNN syntax is used, the index is the value of NNN. + void Bind(Integer integer, int index); + + /// + /// The value to bind. + void Bind(Real real, int index); + + /// + /// The value to bind. + void Bind(Text text, int index); + + /// + /// The value to bind. + void Bind(Blob blob, int index); + + /// + /// + /// Binds a value to the prepared statement. + /// + void BindNull(int index); + + /// + /// You may also call this method with the prepared statement as the argument. + /// Use if your prepared statement returns rows. + Task Execute(); + + /// + /// You may also call this method with the prepared statement as the argument. + /// Use if your prepared statement does not return rows. + Task Query(); + + /// + /// Resets the prepared statement so that it can be executed again. + /// + /// Bound parameters are cleared. + void Reset(); + } +} diff --git a/Libsql.Client/Rows.cs b/Libsql.Client/Rows.cs index c26fb53..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; @@ -123,7 +124,7 @@ private unsafe Value[] ParseRow(libsql_row_t row) _enumeratorData.ColumnTypes[i] == ValueType.Text ? row.GetText(i) : _enumeratorData.ColumnTypes[i] == ValueType.Blob ? row.GetBlob(i) : _enumeratorData.ColumnTypes[i] == ValueType.Null ? (Value)new Null() : - throw new ArgumentOutOfRangeException(); + throw new ArgumentOutOfRangeException($"Non-exhaustive check. Could not find a case to match value of {_enumeratorData.ColumnTypes[i]}"); parsedRow[i] = value; } diff --git a/Libsql.Client/Statement.cs b/Libsql.Client/Statement.cs deleted file mode 100644 index a261f89..0000000 --- a/Libsql.Client/Statement.cs +++ /dev/null @@ -1,94 +0,0 @@ -using System; -using System.Text; - -namespace Libsql.Client -{ - internal class Statement - { - public libsql_stmt_t Stmt; - private libsql_connection_t _connection; - - public unsafe Statement(libsql_connection_t connection, string sql) - { - _connection = connection; - Stmt = new libsql_stmt_t(); - var error = new Error(); - int exitCode; - - fixed (byte* sqlPtr = Encoding.UTF8.GetBytes(sql)) - { - fixed (libsql_stmt_t* statementPtr = &Stmt) - { - exitCode = Bindings.libsql_prepare(_connection, sqlPtr, statementPtr, &error.Ptr); - } - } - - error.ThrowIfNonZero(exitCode, $"Failed to prepare statement for: {sql}"); - } - - public unsafe void Bind(object[] values) - { - var error = new Error(); - int exitCode; - - if (values is null) - { - exitCode = Bindings.libsql_bind_null(Stmt, 1, &error.Ptr); - error.ThrowIfNonZero(exitCode, "Failed to bind null parameter"); - } - else - { - for (var i = 0; i < values.Length; i++) - { - var arg = values[i]; - - - if (arg is int val) { - exitCode = Bindings.libsql_bind_int(Stmt, i + 1, val, &error.Ptr); - } - else if (arg is double d) { - exitCode = Bindings.libsql_bind_float(Stmt, i + 1, d, &error.Ptr); - } - else if (arg is string s) { - fixed (byte* sPtr = Encoding.UTF8.GetBytes(s)) - { - exitCode = Bindings.libsql_bind_string(Stmt, i + 1, sPtr, &error.Ptr); - } - } - else if (arg is byte[] b) { - fixed (byte* bPtr = b) - { - exitCode = Bindings.libsql_bind_blob(Stmt, i + 1, bPtr, b.Length, &error.Ptr); - } - } - else if (arg is null) - { - exitCode = Bindings.libsql_bind_null(Stmt, i + 1, &error.Ptr); - } - else - { - throw new ArgumentException($"Unsupported argument type: {arg.GetType()}"); - } - - error.ThrowIfNonZero(exitCode, $"Failed to bind parameter. Type: {(arg is null ? "null" : arg.GetType().ToString())} Value: {arg}"); - } - } - } - - private void ReleaseUnmanagedResources() - { - Bindings.libsql_free_stmt(Stmt); - } - - public void Dispose() - { - ReleaseUnmanagedResources(); - GC.SuppressFinalize(this); - } - - ~Statement() - { - ReleaseUnmanagedResources(); - } - } -} diff --git a/Libsql.Client/StatementWrapper.cs b/Libsql.Client/StatementWrapper.cs new file mode 100644 index 0000000..22e6c90 --- /dev/null +++ b/Libsql.Client/StatementWrapper.cs @@ -0,0 +1,213 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; +using Libsql.Client.Extensions; + +namespace Libsql.Client +{ + internal class StatementWrapper : IStatement + { + int IStatement.BoundValuesCount => _bindIndex - 1; + + public int ParameterCount => GetParameterCount(); + + private int _bindIndex = 1; + public readonly libsql_stmt_t Stmt; + private readonly libsql_connection_t _connection; + private readonly DatabaseWrapper _database; + + public unsafe StatementWrapper(DatabaseWrapper database, libsql_connection_t connection, string sql) + { + _database = database; + _connection = connection; + Stmt = new libsql_stmt_t(); + var error = new Error(); + int exitCode; + + fixed (byte* sqlPtr = Encoding.UTF8.GetBytes(sql)) + { + fixed (libsql_stmt_t* statementPtr = &Stmt) + { + exitCode = Bindings.libsql_prepare(_connection, sqlPtr, statementPtr, &error.Ptr); + } + } + + error.ThrowIfNonZero(exitCode, $"Failed to prepare statement for: {sql}"); + + Debug.Assert(_database != null); + } + + public unsafe void BindAll(object[] values) + { + if (values is null) + { + var error = new Error(); + var exitCode = Bindings.libsql_bind_null(Stmt, 1, &error.Ptr); + error.ThrowIfNonZero(exitCode, "Failed to bind null parameters"); + } + else + { + for (int i = 1; i <= values.Length; i++) + { + object arg = values[i - 1]; + switch (arg) + { + case int val: + BindInt(val, i); + break; + case double d: + BindFloat(d, i); + break; + case string s: + BindString(s, i); + break; + case byte[] b: + BindBlob(b, i); + break; + case null: + Bind(i); + break; + default: + throw new ArgumentException($"Unsupported argument type: {arg.GetType()}"); + } + } + } + } + + private unsafe void BindInt(int value, int index) + { + var error = new Error(); + var exitCode = Bindings.libsql_bind_int(Stmt, index, value, &error.Ptr); + + error.ThrowIfNonZero(exitCode, $"Failed to bind integer at index {index} with value {value}"); + _bindIndex++; + } + + private unsafe void BindFloat(double value, int index) + { + var error = new Error(); + var exitCode = Bindings.libsql_bind_float(Stmt, index, value, &error.Ptr); + + error.ThrowIfNonZero(exitCode, $"Failed to bind integer at index {index} with value {value}"); + _bindIndex++; + } + + private unsafe void BindString(string value, int index) + { + var error = new Error(); + fixed (byte* sPtr = Encoding.UTF8.GetBytes(value)) { + var exitCode = Bindings.libsql_bind_string(Stmt, index, sPtr, &error.Ptr); + + error.ThrowIfNonZero(exitCode, $"Failed to bind integer at index {index} with value {value}"); + } + _bindIndex++; + } + + private unsafe void BindBlob(byte[] value, int index) + { + var error = new Error(); + fixed (byte* bPtr = value) { + var exitCode = Bindings.libsql_bind_blob(Stmt, index, bPtr, value.Length, &error.Ptr); + + error.ThrowIfNonZero(exitCode, $"Failed to bind integer at index {index} with value {value}"); + } + _bindIndex++; + } + + private unsafe void Bind(int index) + { + var error = new Error(); + var exitCode = Bindings.libsql_bind_null(Stmt, index, &error.Ptr); + + error.ThrowIfNonZero(exitCode, $"Failed to bind null at index {index}"); + _bindIndex++; + } + + private unsafe int GetParameterCount() + { + var error = new Error(); + int count; + var exitCode = Bindings.libsql_stmt_parameter_count(Stmt, &count,&error.Ptr); + + error.ThrowIfNonZero(exitCode, "Failed to get parameter count"); + + return count; + } + + public void Bind(Integer integer, int index) + { + BindInt(integer.Value, index); + } + + public void Bind(Real real, int index) + { + BindFloat(real.Value, index); + } + + public void Bind(Text text, int index) + { + BindString(text.Value, index); + } + + public void Bind(Blob blob, int index) + { + BindBlob(blob.Value, index); + } + + public void BindNull(int index) + { + Bind(index); + } + + public Task Execute() + { + return _database.Execute(this); + } + + public Task Query() + { + return _database.Query(this); + } + + public void Reset() + { + var error = new Error(); + int exitCode; + + unsafe { + exitCode = Bindings.libsql_reset_stmt(Stmt, &error.Ptr); + } + + _bindIndex = 1; + + error.ThrowIfNonZero(exitCode, "Failed to reset statement"); + } + + + private void ReleaseUnmanagedResources() + { + 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); + } + + public void Dispose() + { + ReleaseUnmanagedResources(); + GC.SuppressFinalize(this); + } + + ~StatementWrapper() + { + ReleaseUnmanagedResources(); + } + } +} diff --git a/Libsql.Client/Values/Blob.cs b/Libsql.Client/Values/Blob.cs index 6e1aeed..5c8adc4 100644 --- a/Libsql.Client/Values/Blob.cs +++ b/Libsql.Client/Values/Blob.cs @@ -47,6 +47,10 @@ public override string ToString() public static bool operator !=(Blob left, Blob right) => !(left == right); + public static implicit operator Blob(byte[] value) => new Blob(value); + + public static implicit operator byte[](Blob blob) => blob.Value; + public override bool Equals(Value other) { if (other is Blob otherBlob) diff --git a/Libsql.Client/Values/Integer.cs b/Libsql.Client/Values/Integer.cs index 3e68dd7..b1c0851 100644 --- a/Libsql.Client/Values/Integer.cs +++ b/Libsql.Client/Values/Integer.cs @@ -23,6 +23,10 @@ internal Integer(int value) public static bool operator !=(Integer left, Integer right) => !(left == right); + public static implicit operator Integer(int value) => new Integer(value); + + public static implicit operator int(Integer integer) => integer.Value; + public override bool Equals(Value other) { if (other is Integer otherInteger) diff --git a/Libsql.Client/Values/Real.cs b/Libsql.Client/Values/Real.cs index 7fa6b01..bfd5cfc 100644 --- a/Libsql.Client/Values/Real.cs +++ b/Libsql.Client/Values/Real.cs @@ -24,6 +24,10 @@ internal Real(double value) public static bool operator !=(Real left, Real right) => !(left == right); + public static implicit operator Real(double value) => new Real(value); + + public static implicit operator double(Real integer) => integer.Value; + public override bool Equals(Value other) { if (other is Real otherReal) diff --git a/Libsql.Client/Values/Text.cs b/Libsql.Client/Values/Text.cs index 866c53d..0995af1 100644 --- a/Libsql.Client/Values/Text.cs +++ b/Libsql.Client/Values/Text.cs @@ -22,6 +22,10 @@ internal Text(string value) public static bool operator ==(Text left, Text right) => left?.Value == right?.Value; public static bool operator !=(Text left, Text right) => !(left == right); + + public static implicit operator Text(string value) => new Text(value); + + public static implicit operator string(Text text) => text.Value; public override bool Equals(Value other) { diff --git a/README.md b/README.md index 099c4c5..506f44e 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. A `LibSqlException` will be thrown if you use `Execute` on a statement that returns rows. + +`Query` returns a more useful `IResultSet` object which can be read for additional information such as the number of affected rows, the last inserted row ID, the column names, and the rows themselves. + ### Querying the Database ```csharp @@ -110,11 +123,36 @@ User ToUser(IEnumerable row) throw new ArgumentException(); } -var result = await dbClient.Execute("SELECT * FROM users"); +var result = await dbClient.Query("SELECT * FROM users"); var users = result.Rows.Select(ToUser); ``` +### Prepared Statements + +The following creates a prepared statement. +```csharp +var statement = dbClient.Prepare("SELECT * FROM users where userId = ?"); +``` +You can then bind positional arguments in order. +```csharp +statement.Bind(new Integer(1)) +``` +Statement execution can be done in two ways, both are the same. +```csharp +IResultSet result; +result = statement.Query(); +result = dbClient.Query(statement); +``` +You can also query the number of values already bound to the statement. +```csharp +statement.Bind(0.5); +statement.Bind("libsql"); +var numberOfBoundValues = statement.BoundValuesCount; +Console.WriteLine(numberOfBoundValues) // 2 +``` +> Prepared statements are held resources. `IStatement` implements the `IDisposable` interface. Make sure you manage its lifetime correctly. + ### Closing the Database ```csharp @@ -150,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. diff --git a/devbox.json b/devbox.json new file mode 100644 index 0000000..74df18f --- /dev/null +++ b/devbox.json @@ -0,0 +1,20 @@ +{ + "$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", + "fzf@latest" + ], + "shell": { + "init_hook": [ + "rustup default stable" + ], + "scripts": { + "test": [ + "echo \"Error: no test specified\" && exit 1" + ] + } + } +} diff --git a/devbox.lock b/devbox.lock new file mode 100644 index 0000000..c2effab --- /dev/null +++ b/devbox.lock @@ -0,0 +1,313 @@ +{ + "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" + } + } + }, + "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" + }, + "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/generate-bindings.sh b/generate-bindings.sh index 7feceaf..20a2a6f 100755 --- a/generate-bindings.sh +++ b/generate-bindings.sh @@ -3,17 +3,30 @@ 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) 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 +42,23 @@ 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 + ln -sfn "${local_repo_path}" "${bindings_dir}/libsql" +else + 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}" + fi -if ! git config remote.origin.url &> /dev/null;then - git remote add --no-fetch origin "https://github.com/tursodatabase/libsql.git" + git fetch --quiet origin + git checkout "${commit}" + cd ../.. 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 --depth 1 origin tag libsql-rs-v${LIBSQL_RS_VERSION} -git reset --hard tags/libsql-rs-v${LIBSQL_RS_VERSION} - -cd .. -cargo build --release +cd "${bindings_dir}" +cargo build --lib #--release diff --git a/justfile b/justfile new file mode 100644 index 0000000..6415369 --- /dev/null +++ b/justfile @@ -0,0 +1,11 @@ +default: + @just --choose + +generate-bindings *OPTIONS: + ./generate-bindings.sh {{OPTIONS}} + +test: + dotnet test + +test-file *FILE: + dotnet test --filter "FullyQualifiedName~Libsql.Client.Tests.{{FILE}}" 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 diff --git a/rust-bindings/Cargo.toml b/rust-bindings/Cargo.toml index 7b05dd4..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.32", 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", @@ -16,3 +27,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"] } 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"] } 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 - ''; -}