ソースを参照

feat(orm): add async CRUD methods to ORM session

Add asynchronous versions of insert, update, and delete methods to
OrmSession. These methods perform the same operations as their
synchronous counterparts but yield control back to the main loop
during database operations, enabling non-blocking I/O.

Includes comprehensive tests for all async methods.
clanker 1 ヶ月 前
コミット
7ff662181b
2 ファイル変更378 行追加0 行削除
  1. 149 0
      src/orm/orm-session.vala
  2. 229 0
      src/tests/orm-test.vala

+ 149 - 0
src/orm/orm-session.vala

@@ -124,6 +124,67 @@ namespace InvercargillSql.Orm {
             }
         }
         
+        /**
+         * Inserts an entity into the database asynchronously.
+         *
+         * This method performs the same operation as insert() but in a non-blocking
+         * manner, allowing the main loop to process other events while waiting
+         * for the database operation to complete.
+         *
+         * After insertion, if the entity has an auto-increment primary key,
+         * the key value is back-populated into the entity.
+         *
+         * @param entity The entity to insert
+         * @throws SqlError if insertion fails
+         */
+        public async void insert_async<T>(T entity) throws Error {
+            var mapper = get_mapper<T>();
+            Invercargill.Properties properties;
+            try {
+                properties = mapper.map_from(entity);
+            } catch (Error e) {
+                throw new SqlError.GENERAL_ERROR("Failed to map entity: %s".printf(e.message));
+            }
+            
+            // Build column list, excluding auto-increment columns
+            var columns = new Vector<string>();
+            foreach (var col in mapper.columns) {
+                if (!mapper.is_auto_increment(col.name)) {
+                    columns.add(col.name);
+                }
+            }
+            
+            var sql = _dialect.build_insert_sql(mapper.table_name, columns);
+            var command = _connection.create_command(sql);
+            
+            // Add parameters in column order (excluding auto-increment)
+            foreach (var col in mapper.columns) {
+                if (mapper.is_auto_increment(col.name)) {
+                    continue;  // Skip auto-increment columns
+                }
+                var value = properties.get(col.name);
+                if (value != null) {
+                    command.with_parameter<Invercargill.Element>(col.name, value);
+                } else {
+                    command.with_null(col.name);
+                }
+            }
+            
+            // Execute asynchronously
+            yield command.execute_non_query_async();
+            
+            // Back-populate the generated primary key
+            var pk_column = mapper.get_effective_primary_key();
+            if (pk_column != null && mapper.is_auto_increment(pk_column)) {
+                int64 generated_id = _connection.last_insert_rowid;
+                try {
+                    mapper.set_property_value(entity, pk_column, generated_id);
+                } catch (Error e) {
+                    throw new SqlError.GENERAL_ERROR("Failed to back-populate primary key: %s".printf(e.message));
+                }
+            }
+        }
+        
         /**
          * Updates an entity in the database.
          * 
@@ -169,6 +230,58 @@ namespace InvercargillSql.Orm {
             command.execute_non_query();
         }
         
+        /**
+         * Updates an entity in the database asynchronously.
+         *
+         * This method performs the same operation as update() but in a non-blocking
+         * manner, allowing the main loop to process other events while waiting
+         * for the database operation to complete.
+         *
+         * The entity is identified by its primary key.
+         *
+         * @param entity The entity to update
+         * @throws SqlError if update fails
+         */
+        public async void update_async<T>(T entity) throws Error {
+            var mapper = get_mapper<T>();
+            Invercargill.Properties properties;
+            try {
+                properties = mapper.map_from(entity);
+            } catch (Error e) {
+                throw new SqlError.GENERAL_ERROR("Failed to map entity: %s".printf(e.message));
+            }
+            
+            var columns = new Vector<string>();
+            foreach (var col in mapper.columns) {
+                columns.add(col.name);
+            }
+            
+            var pk_column = mapper.get_effective_primary_key();
+            var sql = _dialect.build_update_sql(mapper.table_name, columns, pk_column);
+            var command = _connection.create_command(sql);
+            
+            // Add parameters for SET clause
+            foreach (var col in mapper.columns) {
+                var value = properties.get(col.name);
+                if (value != null) {
+                    command.with_parameter<Invercargill.Element>(col.name, value);
+                } else {
+                    command.with_null(col.name);
+                }
+            }
+            
+            // Add primary key parameter for WHERE clause
+            var pk_value = properties.get(pk_column);
+            if (pk_value != null) {
+                command.with_parameter<Invercargill.Element>(pk_column, pk_value);
+            } else {
+                command.with_null(pk_column);
+            }
+            
+            // Execute asynchronously
+            yield command.execute_non_query_async();
+        }
+        
         /**
          * Deletes an entity from the database.
          * 
@@ -198,6 +311,42 @@ namespace InvercargillSql.Orm {
             command.execute_non_query();
         }
         
+        /**
+         * Deletes an entity from the database asynchronously.
+         *
+         * This method performs the same operation as delete() but in a non-blocking
+         * manner, allowing the main loop to process other events while waiting
+         * for the database operation to complete.
+         *
+         * The entity is identified by its primary key.
+         *
+         * @param entity The entity to delete
+         * @throws SqlError if deletion fails
+         */
+        public async void delete_async<T>(T entity) throws Error {
+            var mapper = get_mapper<T>();
+            Invercargill.Properties properties;
+            try {
+                properties = mapper.map_from(entity);
+            } catch (Error e) {
+                throw new SqlError.GENERAL_ERROR("Failed to map entity: %s".printf(e.message));
+            }
+            
+            var pk_column = mapper.get_effective_primary_key();
+            var sql = _dialect.build_delete_sql(mapper.table_name, pk_column);
+            var command = _connection.create_command(sql);
+            
+            var pk_value = properties.get(pk_column);
+            if (pk_value != null) {
+                command.with_parameter<Invercargill.Element>(pk_column, pk_value);
+            } else {
+                command.with_null(pk_column);
+            }
+            
+            // Execute asynchronously
+            yield command.execute_non_query_async();
+        }
+        
         /**
          * Gets the SQL dialect for internal use.
          * Used by EntityQuery and ProjectionQuery to build SQL.

+ 229 - 0
src/tests/orm-test.vala

@@ -168,6 +168,13 @@ public int main(string[] args) {
         test_insert_back_populates_product();
         test_insert_back_populates_order();
         
+        // Async CRUD method tests
+        print("\n--- Async CRUD Method Tests ---\n");
+        run_test_insert_async_basic();
+        run_test_insert_async_yields();
+        run_test_update_async_basic();
+        run_test_delete_async_basic();
+        
         print("\n=== All ORM tests passed! ===\n");
         return 0;
     } catch (Error e) {
@@ -1442,3 +1449,225 @@ void test_insert_back_populates_order() throws Error {
     
     print("PASSED\n");
 }
+
+// ========================================
+// Async CRUD Method Tests
+// ========================================
+
+/**
+ * Test: Async insert basic functionality.
+ *
+ * Verifies that insert_async correctly inserts an entity and
+ * back-populates the auto-generated primary key.
+ */
+async void test_insert_async_basic() throws Error {
+    print("Test: insert_async basic... ");
+    var session = setup_test_session();
+    
+    var user = new TestUser();
+    user.name = "AsyncUser";
+    user.email = "async@example.com";
+    user.age = 30;
+    
+    assert(user.id == 0);
+    
+    yield session.insert_async(user);
+    
+    // Verify back-population
+    assert(user.id > 0);
+    
+    // Verify data was persisted
+    var results = session.query<TestUser>()
+        .where("id == " + user.id.to_string())
+        .materialise();
+    var arr = results.to_array();
+    assert(arr.length == 1);
+    assert(arr[0].name == "AsyncUser");
+    
+    print("PASSED\n");
+}
+
+/**
+ * Test: Async insert yields to main loop.
+ *
+ * Verifies that insert_async properly yields control back to the main loop
+ * during the database operation, allowing other events to be processed.
+ */
+async void test_insert_async_yields() throws Error {
+    print("Test: insert_async yields... ");
+    var session = setup_test_session();
+    
+    bool callback_executed = false;
+    var source = new IdleSource();
+    source.set_callback(() => {
+        callback_executed = true;
+        return Source.REMOVE;
+    });
+    source.attach(MainContext.default());
+    
+    var user = new TestUser();
+    user.name = "YieldTest";
+    user.email = "yield@example.com";
+    user.age = 25;
+    
+    yield session.insert_async(user);
+    
+    // If async properly yielded, the idle callback should have executed
+    assert(callback_executed);
+    
+    print("PASSED\n");
+}
+
+/**
+ * Test: Async update basic functionality.
+ *
+ * Verifies that update_async correctly updates an existing entity.
+ */
+async void test_update_async_basic() throws Error {
+    print("Test: update_async basic... ");
+    var session = setup_test_session();
+    
+    // Insert a user first
+    var user = new TestUser();
+    user.name = "Original";
+    user.email = "original@example.com";
+    user.age = 25;
+    session.insert(user);
+    
+    // Update the user
+    user.name = "Updated";
+    user.age = 26;
+    yield session.update_async(user);
+    
+    // Verify update
+    var updated = session.query<TestUser>()
+        .where("id == " + user.id.to_string())
+        .first();
+    assert(updated != null);
+    assert(updated.name == "Updated");
+    assert(updated.age == 26);
+    
+    print("PASSED\n");
+}
+
+/**
+ * Test: Async delete basic functionality.
+ *
+ * Verifies that delete_async correctly deletes an entity from the database.
+ */
+async void test_delete_async_basic() throws Error {
+    print("Test: delete_async basic... ");
+    var session = setup_test_session();
+    
+    // Insert a user first
+    var user = new TestUser();
+    user.name = "ToDelete";
+    user.email = "delete@example.com";
+    user.age = 99;
+    session.insert(user);
+    var user_id = user.id;
+    
+    // Delete the user
+    yield session.delete_async(user);
+    
+    // Verify deletion
+    var results = session.query<TestUser>()
+        .where("id == " + user_id.to_string())
+        .materialise();
+    var arr = results.to_array();
+    assert(arr.length == 0);
+    
+    print("PASSED\n");
+}
+
+/**
+ * Synchronous wrapper for test_insert_async_basic.
+ */
+void run_test_insert_async_basic() throws Error {
+    var loop = new MainLoop();
+    Error? error = null;
+    
+    test_insert_async_basic.begin((obj, res) => {
+        try {
+            test_insert_async_basic.end(res);
+        } catch (Error e) {
+            error = e;
+        }
+        loop.quit();
+    });
+    
+    loop.run();
+    
+    if (error != null) {
+        throw error;
+    }
+}
+
+/**
+ * Synchronous wrapper for test_insert_async_yields.
+ */
+void run_test_insert_async_yields() throws Error {
+    var loop = new MainLoop();
+    Error? error = null;
+    
+    test_insert_async_yields.begin((obj, res) => {
+        try {
+            test_insert_async_yields.end(res);
+        } catch (Error e) {
+            error = e;
+        }
+        loop.quit();
+    });
+    
+    loop.run();
+    
+    if (error != null) {
+        throw error;
+    }
+}
+
+/**
+ * Synchronous wrapper for test_update_async_basic.
+ */
+void run_test_update_async_basic() throws Error {
+    var loop = new MainLoop();
+    Error? error = null;
+    
+    test_update_async_basic.begin((obj, res) => {
+        try {
+            test_update_async_basic.end(res);
+        } catch (Error e) {
+            error = e;
+        }
+        loop.quit();
+    });
+    
+    loop.run();
+    
+    if (error != null) {
+        throw error;
+    }
+}
+
+/**
+ * Synchronous wrapper for test_delete_async_basic.
+ */
+void run_test_delete_async_basic() throws Error {
+    var loop = new MainLoop();
+    Error? error = null;
+    
+    test_delete_async_basic.begin((obj, res) => {
+        try {
+            test_delete_async_basic.end(res);
+        } catch (Error e) {
+            error = e;
+        }
+        loop.quit();
+    });
+    
+    loop.run();
+    
+    if (error != null) {
+        throw error;
+    }
+}