diff --git a/AGENTS.md b/AGENTS.md index 0b4bf2f8..67375238 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -494,10 +494,11 @@ IO.puts("Statement expects #{param_count} parameter(s)") # Prints: 1 IO.puts("Result will have #{col_count} column(s)") # Prints: 4 # Get column names -{:ok, col_names} = Enum.map(0..(col_count-1), fn i -> - {:ok, name} = EctoLibSql.Native.statement_column_name(state, stmt_id, i) - name -end) +col_names = + Enum.map(0..(col_count - 1), fn i -> + {:ok, name} = EctoLibSql.Native.statement_column_name(state, stmt_id, i) + name + end) IO.inspect(col_names) # Prints: ["id", "name", "email", "created_at"] :ok = EctoLibSql.Native.close_stmt(stmt_id) diff --git a/IMPLEMENTATION_ROADMAP_FOCUSED.md b/IMPLEMENTATION_ROADMAP_FOCUSED.md index 39c1baaa..2b0b2fca 100644 --- a/IMPLEMENTATION_ROADMAP_FOCUSED.md +++ b/IMPLEMENTATION_ROADMAP_FOCUSED.md @@ -195,10 +195,9 @@ Logger.info("Current replication frame: #{current_frame}") ``` **libsql API**: -- `database.sync_until(frame_no)` - Sync until specific frame -- `database.get_frame_no()` - Get current frame number -- `database.flush_replicator()` - Flush pending replication -- `database.sync_frames(count)` - Sync specific number of frames +- `replication_index()` - Get current frame number +- `sync()` / `sync_until(frame_no)` - Sync replica until specific frame +- `flush_replicator()` - Flush pending replication **Implementation**: - [x] Add `sync_until(conn_id, frame_no)` NIF diff --git a/native/ecto_libsql/src/lib.rs b/native/ecto_libsql/src/lib.rs index 1a1b973d..8593eab8 100644 --- a/native/ecto_libsql/src/lib.rs +++ b/native/ecto_libsql/src/lib.rs @@ -145,6 +145,45 @@ fn decode_transaction_behavior(atom: Atom) -> Option { } } +/// Helper function to verify transaction ownership. +/// +/// Returns an error if the transaction does not belong to the specified connection. +fn verify_transaction_ownership( + entry: &TransactionEntry, + conn_id: &str, +) -> Result<(), rustler::Error> { + if entry.conn_id != conn_id { + return Err(rustler::Error::Term(Box::new( + "Transaction does not belong to this connection", + ))); + } + Ok(()) +} + +/// Helper function to verify statement ownership. +/// +/// Returns an error if the statement does not belong to the specified connection. +fn verify_statement_ownership(stmt_conn_id: &str, conn_id: &str) -> Result<(), rustler::Error> { + if stmt_conn_id != conn_id { + return Err(rustler::Error::Term(Box::new( + "Statement does not belong to connection", + ))); + } + Ok(()) +} + +/// Helper function to verify cursor ownership. +/// +/// Returns an error if the cursor does not belong to the specified connection. +fn verify_cursor_ownership(cursor: &CursorData, conn_id: &str) -> Result<(), rustler::Error> { + if cursor.conn_id != conn_id { + return Err(rustler::Error::Term(Box::new( + "Cursor does not belong to connection", + ))); + } + Ok(()) +} + #[rustler::nif(schedule = "DirtyIo")] pub fn begin_transaction(conn_id: &str) -> NifResult { let conn_map = safe_lock(&CONNECTION_REGISTRY, "begin_transaction conn_map")?; @@ -226,11 +265,7 @@ pub fn execute_with_transaction<'a>( .ok_or_else(|| rustler::Error::Term(Box::new("Transaction not found")))?; // Verify transaction belongs to this connection - if entry.conn_id != conn_id { - return Err(rustler::Error::Term(Box::new( - "Transaction does not belong to this connection", - ))); - } + verify_transaction_ownership(entry, conn_id)?; let decoded_args: Vec = args .into_iter() @@ -260,11 +295,7 @@ pub fn query_with_trx_args<'a>( .ok_or_else(|| rustler::Error::Term(Box::new("Transaction not found")))?; // Verify transaction belongs to this connection - if entry.conn_id != conn_id { - return Err(rustler::Error::Term(Box::new( - "Transaction does not belong to this connection", - ))); - } + verify_transaction_ownership(entry, conn_id)?; let decoded_args: Vec = args .into_iter() @@ -941,11 +972,7 @@ fn query_prepared<'a>( .ok_or_else(|| rustler::Error::Term(Box::new("Statement not found")))?; // Verify statement belongs to this connection - if stored_conn_id != conn_id { - return Err(rustler::Error::Term(Box::new( - "Statement does not belong to connection", - ))); - } + verify_statement_ownership(stored_conn_id, conn_id)?; let cached_stmt = cached_stmt.clone(); @@ -1005,11 +1032,7 @@ fn execute_prepared<'a>( .ok_or_else(|| rustler::Error::Term(Box::new("Statement not found")))?; // Verify statement belongs to this connection - if stored_conn_id != conn_id { - return Err(rustler::Error::Term(Box::new( - "Statement does not belong to connection", - ))); - } + verify_statement_ownership(stored_conn_id, conn_id)?; let cached_stmt = cached_stmt.clone(); @@ -1341,11 +1364,7 @@ fn fetch_cursor<'a>( .ok_or_else(|| rustler::Error::Term(Box::new("Cursor not found")))?; // Verify cursor belongs to this connection - if cursor.conn_id != conn_id { - return Err(rustler::Error::Term(Box::new( - "Cursor does not belong to connection", - ))); - } + verify_cursor_ownership(cursor, conn_id)?; let remaining = cursor.rows.len().saturating_sub(cursor.position); let fetch_count = remaining.min(max_rows); @@ -1623,11 +1642,7 @@ fn statement_column_count(conn_id: &str, stmt_id: &str) -> NifResult { .ok_or_else(|| rustler::Error::Term(Box::new("Statement not found")))?; // Verify statement belongs to this connection - if stored_conn_id != conn_id { - return Err(rustler::Error::Term(Box::new( - "Statement does not belong to connection", - ))); - } + verify_statement_ownership(stored_conn_id, conn_id)?; let cached_stmt = cached_stmt.clone(); @@ -1656,11 +1671,7 @@ fn statement_column_name(conn_id: &str, stmt_id: &str, idx: usize) -> NifResult< .ok_or_else(|| rustler::Error::Term(Box::new("Statement not found")))?; // Verify statement belongs to this connection - if stored_conn_id != conn_id { - return Err(rustler::Error::Term(Box::new( - "Statement does not belong to connection", - ))); - } + verify_statement_ownership(stored_conn_id, conn_id)?; let cached_stmt = cached_stmt.clone(); @@ -1699,11 +1710,7 @@ fn statement_parameter_count(conn_id: &str, stmt_id: &str) -> NifResult { .ok_or_else(|| rustler::Error::Term(Box::new("Statement not found")))?; // Verify statement belongs to this connection - if stored_conn_id != conn_id { - return Err(rustler::Error::Term(Box::new( - "Statement does not belong to connection", - ))); - } + verify_statement_ownership(stored_conn_id, conn_id)?; let cached_stmt = cached_stmt.clone();