Index: src/bp-xprofile/bp-xprofile-classes.php
--- src/bp-xprofile/bp-xprofile-classes.php
+++ src/bp-xprofile/bp-xprofile-classes.php
@@ -23,4 +23,5 @@
 require dirname( __FILE__ ) . '/classes/class-bp-xprofile-field-type-number.php';
 require dirname( __FILE__ ) . '/classes/class-bp-xprofile-field-type-url.php';
 require dirname( __FILE__ ) . '/classes/class-bp-xprofile-field-type-placeholder.php';
+require dirname( __FILE__ ) . '/classes/class-bp-xprofile-meta-query.php';
 require dirname( __FILE__ ) . '/classes/class-bp-xprofile-query.php';
Index: src/bp-xprofile/bp-xprofile-filters.php
--- src/bp-xprofile/bp-xprofile-filters.php
+++ src/bp-xprofile/bp-xprofile-filters.php
@@ -69,7 +69,43 @@
 add_filter( 'xprofile_field_option_order_before_save', 'absint' );
 add_filter( 'xprofile_field_can_delete_before_save',   'absint' );
 
+// Save field options
+add_filter( 'xprofile_field_options_before_save', 'bp_xprofile_sanitize_field_options' );
+add_filter( 'xprofile_field_default_before_save', 'bp_xprofile_sanitize_field_default' );
+
 /**
+ * Sanitize each field option name for saving to the database
+ *
+ * @since BuddyPress (2.3.0)
+ *
+ * @param  mixed $field_options
+ * @return mixed
+ */
+function bp_xprofile_sanitize_field_options( $field_options = '' ) {
+	if ( is_array( $field_options ) ) {
+		return array_map( 'sanitize_text_field', $field_options );
+	} else {
+		return sanitize_text_field( $field_options );
+	}
+}
+
+/**
+ * Sanitize each field option default for saving to the database
+ *
+ * @since BuddyPress (2.3.0)
+ *
+ * @param  mixed $field_default
+ * @return mixed
+ */
+function bp_xprofile_sanitize_field_default( $field_default = '' ) {
+	if ( is_array( $field_default ) ) {
+		return array_map( 'intval', $field_default );
+	} else {
+		return intval( $field_default );
+	}
+}
+
+/**
  * xprofile_filter_kses ( $content )
  *
  * Run profile field values through kses with filterable allowed tags.
Index: src/bp-xprofile/bp-xprofile-functions.php
--- src/bp-xprofile/bp-xprofile-functions.php
+++ src/bp-xprofile/bp-xprofile-functions.php
@@ -968,6 +968,42 @@
 }
 
 /**
+ * Get array of field IDs to show on member registration page
+ *
+ * @since BuddyPress (2.3.0)
+ *
+ * @return array
+ */
+function bp_xprofile_get_signup_field_ids() {
+
+	// Query for specificly set signup fields
+	$table_name = buddypress()->profile->table_name_meta;
+	$fields     = BP_XProfile_Field::get_fields( array(
+		'meta_query' => array(
+			array(
+				'key'     => 'signup_position',
+				'object'  => 'field',
+				'compare' => '='
+			)
+		),
+		'order_by' => "{$table_name}.meta_value"
+	) );
+
+	// No signup fields have been set, so query for all fields in the primary
+	// group ID
+	if ( empty( $fields ) ) {
+		$fields = BP_XProfile_Field::get_fields( array(
+			'group_id' => '1'
+		) );
+	}
+
+	// Pluck the ID's from the fields
+	$field_ids = wp_list_pluck( $fields, 'id' );
+
+	return apply_filters( 'bp_xprofile_get_signup_field_ids', $field_ids, $fields );
+}
+
+/**
  * Return the field ID for the Full Name xprofile field.
  *
  * @since BuddyPress (2.0.0)
Index: src/bp-xprofile/bp-xprofile-loader.php
--- src/bp-xprofile/bp-xprofile-loader.php
+++ src/bp-xprofile/bp-xprofile-loader.php
@@ -367,6 +367,7 @@
 		wp_cache_add_global_groups( array(
 			'bp_xprofile',
 			'bp_xprofile_data',
+			'bp_xprofile_fields',
 			'bp_xprofile_groups',
 			'xprofile_meta'
 		) );
Index: src/bp-xprofile/classes/class-bp-xprofile-field.php
--- src/bp-xprofile/classes/class-bp-xprofile-field.php
+++ src/bp-xprofile/classes/class-bp-xprofile-field.php
@@ -110,6 +110,13 @@
 	public $allow_custom_visibility = 'allowed';
 
 	/**
+	 * @since BuddyPress (2.3.0)
+	 *
+	 * @var int Position of field on user registration page
+	 */
+	public $signup_position = null;
+
+	/**
 	 * @since BuddyPress (2.0.0)
 	 *
 	 * @var BP_XProfile_Field_Type Field type object used for validation
@@ -157,85 +164,79 @@
 	 * @param  bool   $get_data
 	 */
 	public function populate( $id, $user_id = null, $get_data = true ) {
-		global $wpdb, $userdata;
 
-		if ( empty( $user_id ) ) {
-			$user_id = isset( $userdata->ID ) ? $userdata->ID : 0;
-		}
+		// Check for cached field
+		$field = wp_cache_get( $id, 'bp_xprofile_fields' );
 
-		$bp    = buddypress();
-		$field = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$bp->profile->table_name_fields} WHERE id = %d", $id ) );
+		// Field not cached, so query (and subsequently cache)
+		if ( false === $field ) {
 
-		if ( ! empty( $field ) ) {
-			$this->id                = $field->id;
-			$this->group_id          = $field->group_id;
-			$this->parent_id         = $field->parent_id;
-			$this->type              = $field->type;
-			$this->name              = stripslashes( $field->name );
-			$this->description       = stripslashes( $field->description );
-			$this->is_required       = $field->is_required;
-			$this->can_delete        = $field->can_delete;
-			$this->field_order       = $field->field_order;
-			$this->option_order      = $field->option_order;
-			$this->order_by          = $field->order_by;
-			$this->is_default_option = $field->is_default_option;
+			// Setup args
+			$args = array(
+				'include' => $id
+			);
 
-			// Create the field type and store a reference back to this object.
-			$this->type_obj            = bp_xprofile_create_field_type( $field->type );
-			$this->type_obj->field_obj = $this;
-
-			if ( ! empty( $get_data ) && ! empty( $user_id ) ) {
-				$this->data = $this->get_field_data( $user_id );
+			// Get data for the user ID
+			if ( ! empty( $user_id ) && ! empty( $get_data ) ) {
+				$args['fetch_data'] = $user_id;
 			}
 
-			// Get metadata for field
-			$default_visibility       = bp_xprofile_get_meta( $id, 'field', 'default_visibility'      );
-			$allow_custom_visibility  = bp_xprofile_get_meta( $id, 'field', 'allow_custom_visibility' );
+			// Get the field and its data
+			$field = self::get_fields( $args );
 
-			// Setup default visibility
-			$this->default_visibility = ! empty( $default_visibility )
-				? $default_visibility
-				: 'public';
+			// Use the first item if an array
+			if ( array( $field ) ) {
+				$field = reset( $field );
+			}
+		}
 
-			// Allow members to customize visibilty
-			$this->allow_custom_visibility = ( 'disabled' === $allow_custom_visibility )
-				? 'disabled'
-				: 'allowed';
+		// Bail if field could not be found
+		if ( empty( $field ) ) {
+			return;
 		}
-	}
 
-	/**
-	 * Delete a profile field
-	 *
-	 * @since BuddyPress (1.1.0)
-	 *
-	 * @global object  $wpdb
-	 * @param  boolean $delete_data
-	 * @return boolean
-	 */
-	public function delete( $delete_data = false ) {
-		global $wpdb;
+		// Setup this field
+		$this->id                = $field->id;
+		$this->group_id          = $field->group_id;
+		$this->parent_id         = $field->parent_id;
+		$this->type              = $field->type;
+		$this->name              = stripslashes( $field->name );
+		$this->description       = stripslashes( $field->description );
+		$this->is_required       = $field->is_required;
+		$this->is_default_option = $field->is_default_option;
+		$this->field_order       = $field->field_order;
+		$this->option_order      = $field->option_order;
+		$this->order_by          = $field->order_by;
+		$this->can_delete        = $field->can_delete;
 
-		// Prevent deletion if no ID is present
-		// Prevent deletion by url when can_delete is false.
-		// Prevent deletion of option 1 since this invalidates fields with options.
-		if ( empty( $this->id ) || empty( $this->can_delete ) || ( $this->parent_id && $this->option_order == 1 ) ) {
-			return false;
+		// Set field data if it was requested
+		if ( ! empty( $field->data ) ) {
+			$this->data = $field->data;
 		}
 
-		$bp  = buddypress();
-		$sql = $wpdb->prepare( "DELETE FROM {$bp->profile->table_name_fields} WHERE id = %d OR parent_id = %d", $this->id, $this->id );
+		// Create the field type and store a reference back to this object.
+		$this->type_obj            = bp_xprofile_create_field_type( $field->type );
+		$this->type_obj->field_obj = $this;
 
-		if ( ! $wpdb->query( $sql ) ) {
-			return false;
-		}
+		// Get core metadata for field
+		$default_visibility      = bp_xprofile_get_meta( $id, 'field', 'default_visibility'      );
+		$allow_custom_visibility = bp_xprofile_get_meta( $id, 'field', 'allow_custom_visibility' );
+		$signup_position         = bp_xprofile_get_meta( $id, 'field', 'signup_position'         );
 
-		// delete the data in the DB for this field
-		if ( true === $delete_data ) {
-			BP_XProfile_ProfileData::delete_for_field( $this->id );
-		}
+		// Setup default visibility
+		$this->default_visibility = ! empty( $default_visibility )
+			? $default_visibility
+			: 'public';
 
-		return true;
+		// Allow members to customize visibilty
+		$this->allow_custom_visibility = ( 'disabled' === $allow_custom_visibility )
+			? 'disabled'
+			: 'allowed';
+
+		// Is this field used on the registration page
+		$this->signup_position = ( false !== $signup_position )
+			? $signup_position
+			: null;
 	}
 
 	/**
@@ -250,20 +251,19 @@
 	public function save() {
 		global $wpdb;
 
-		$bp = buddypress();
+		// Filter field variables before they are saved
+		$this->group_id          = apply_filters( 'xprofile_field_group_id_before_save',          $this->group_id,          $this->id );
+		$this->parent_id         = apply_filters( 'xprofile_field_parent_id_before_save',         $this->parent_id,         $this->id );
+		$this->type              = apply_filters( 'xprofile_field_type_before_save',              $this->type,              $this->id );
+		$this->name              = apply_filters( 'xprofile_field_name_before_save',              $this->name,              $this->id );
+		$this->description       = apply_filters( 'xprofile_field_description_before_save',       $this->description,       $this->id );
+		$this->is_required       = apply_filters( 'xprofile_field_is_required_before_save',       $this->is_required,       $this->id );
+		$this->is_default_option = apply_filters( 'xprofile_field_is_default_option_before_save', $this->is_default_option, $this->id );
+		$this->field_order       = apply_filters( 'xprofile_field_field_order_before_save',       $this->field_order,       $this->id );
+		$this->option_order      = apply_filters( 'xprofile_field_option_order_before_save',      $this->option_order,      $this->id );
+		$this->order_by	         = apply_filters( 'xprofile_field_order_by_before_save',          $this->order_by,          $this->id );
+		$this->can_delete        = apply_filters( 'xprofile_field_can_delete_before_save',        $this->can_delete,        $this->id );
 
-		$this->group_id     = apply_filters( 'xprofile_field_group_id_before_save',     $this->group_id,     $this->id );
-		$this->parent_id    = apply_filters( 'xprofile_field_parent_id_before_save',    $this->parent_id,    $this->id );
-		$this->type         = apply_filters( 'xprofile_field_type_before_save',         $this->type,         $this->id );
-		$this->name         = apply_filters( 'xprofile_field_name_before_save',         $this->name,         $this->id );
-		$this->description  = apply_filters( 'xprofile_field_description_before_save',  $this->description,  $this->id );
-		$this->is_required  = apply_filters( 'xprofile_field_is_required_before_save',  $this->is_required,  $this->id );
-		$this->order_by	    = apply_filters( 'xprofile_field_order_by_before_save',     $this->order_by,     $this->id );
-		$this->field_order  = apply_filters( 'xprofile_field_field_order_before_save',  $this->field_order,  $this->id );
-		$this->option_order = apply_filters( 'xprofile_field_option_order_before_save', $this->option_order, $this->id );
-		$this->can_delete   = apply_filters( 'xprofile_field_can_delete_before_save',   $this->can_delete,   $this->id );
-		$this->type_obj     = bp_xprofile_create_field_type( $this->type );
-
 		/**
 		 * Fires before the current field instance gets saved.
 		 *
@@ -275,116 +275,57 @@
 		 */
 		do_action_ref_array( 'xprofile_field_before_save', array( $this ) );
 
-		if ( $this->id != null ) {
-			$sql = $wpdb->prepare( "UPDATE {$bp->profile->table_name_fields} SET group_id = %d, parent_id = 0, type = %s, name = %s, description = %s, is_required = %d, order_by = %s, field_order = %d, option_order = %d, can_delete = %d WHERE id = %d", $this->group_id, $this->type, $this->name, $this->description, $this->is_required, $this->order_by, $this->field_order, $this->option_order, $this->can_delete, $this->id );
+		// Get the profile field table name
+		$table_name = buddypress()->profile->table_name_fields;
+
+		// Existing field
+		if ( ! empty( $this->id ) ) {
+			$sql = $wpdb->prepare( "UPDATE {$table_name} SET group_id = %d, parent_id = %d, type = %s, name = %s, description = %s, is_required = %d, is_default_option = %d, order_by = %s, field_order = %d, option_order = %d, can_delete = %d WHERE id = %d", $this->group_id, $this->parent_id, $this->type, $this->name, $this->description, $this->is_required, $this->is_default_option, $this->order_by, $this->field_order, $this->option_order, $this->can_delete, $this->id );
+
+		// New field
 		} else {
-			$sql = $wpdb->prepare( "INSERT INTO {$bp->profile->table_name_fields} (group_id, parent_id, type, name, description, is_required, order_by, field_order, option_order, can_delete ) VALUES (%d, %d, %s, %s, %s, %d, %s, %d, %d, %d )", $this->group_id, $this->parent_id, $this->type, $this->name, $this->description, $this->is_required, $this->order_by, $this->field_order, $this->option_order, $this->can_delete );
+			$sql = $wpdb->prepare( "INSERT INTO {$table_name} (group_id, parent_id, type, name, description, is_required, is_default_option, order_by, field_order, option_order, can_delete ) VALUES (%d, %d, %s, %s, %s, %d, %d, %s, %d, %d, %d )",             $this->group_id, $this->parent_id, $this->type, $this->name, $this->description, $this->is_required, $this->is_default_option, $this->order_by, $this->field_order, $this->option_order, $this->can_delete );
 		}
 
+		// Attempt to update or insert
+		$query = $wpdb->query( $sql );
+
 		/**
-		 * Check for null so field options can be changed without changing any
-		 * other part of the field. The described situation will return 0 here.
+		 * Check for `null` instead of `false` or `empty()` to allow child
+		 * field-options to be modified even if no other field properties changed.
 		 */
-		if ( $wpdb->query( $sql ) !== null ) {
+		if ( ( null === $query ) || is_wp_error( $query ) ) {
+			return false;
+		}
 
-			if ( !empty( $this->id ) ) {
-				$field_id = $this->id;
-			} else {
-				$field_id = $wpdb->insert_id;
-			}
+		// Set the field ID on INSERT
+		if ( empty( $this->id ) ) {
+			$this->id = $wpdb->insert_id;
+		}
 
-			// Only do this if we are editing an existing field
-			if ( $this->id != null ) {
+		// Setup the field's field-type object immediately before cache actions
+		// and child-field updates take place, to ensure all necessary field
+		// data is available to these methods.
+		$this->type_obj            = bp_xprofile_create_field_type( $this->type );
+		$this->type_obj->field_obj = $this;
 
-				/**
-				 * Remove any radio or dropdown options for this
-				 * field. They will be re-added if needed.
-				 * This stops orphan options if the user changes a
-				 * field from a radio button field to a text box.
-				 */
-				$this->delete_children();
-			}
+		// Delete the relevant caches
+		$this->purge_caches();
 
-			/**
-			 * Check to see if this is a field with child options.
-			 * We need to add the options to the db, if it is.
-			 */
-			if ( $this->type_obj->supports_options ) {
+		// Maybe update child fields
+		$this->update_field_options();
 
-				if ( !empty( $this->id ) ) {
-					$parent_id = $this->id;
-				} else {
-					$parent_id = $wpdb->insert_id;
-				}
+		/**
+		 * Fires after the current field instance gets saved.
+		 *
+		 * @since BuddyPress (1.0.0)
+		 *
+		 * @param BP_XProfile_Field Current instance of the field being saved.
+		 */
+		do_action_ref_array( 'xprofile_field_after_save', array( $this ) );
 
-				// Allow plugins to filter the field's child options (i.e. the items in a selectbox).
-				$post_option  = ! empty( $_POST["{$this->type}_option"]           ) ? $_POST["{$this->type}_option"]           : '';
-				$post_default = ! empty( $_POST["isDefault_{$this->type}_option"] ) ? $_POST["isDefault_{$this->type}_option"] : '';
-
-				/**
-				 * Filters the submitted field option value before saved.
-				 *
-				 * @since BuddyPress (1.5.0)
-				 *
-				 * @param string            $post_option Submitted option value.
-				 * @param BP_XProfile_Field $type        Current field type being saved for.
-				 */
-				$options      = apply_filters( 'xprofile_field_options_before_save', $post_option,  $this->type );
-
-				/**
-				 * Filters the default field option value before saved.
-				 *
-				 * @since BuddyPress (1.5.0)
-				 *
-				 * @param string            $post_default Default option value.
-				 * @param BP_XProfile_Field $type         Current field type being saved for.
-				 */
-				$defaults     = apply_filters( 'xprofile_field_default_before_save', $post_default, $this->type );
-
-				$counter = 1;
-				if ( !empty( $options ) ) {
-					foreach ( (array) $options as $option_key => $option_value ) {
-						$is_default = 0;
-
-						if ( is_array( $defaults ) ) {
-							if ( isset( $defaults[ $option_key ] ) ) {
-								$is_default = 1;
-							}
-						} else {
-							if ( (int) $defaults == $option_key ) {
-								$is_default = 1;
-							}
-						}
-
-						if ( '' != $option_value ) {
-							$sql = $wpdb->prepare( "INSERT INTO {$bp->profile->table_name_fields} (group_id, parent_id, type, name, description, is_required, option_order, is_default_option) VALUES (%d, %d, 'option', %s, '', 0, %d, %d)", $this->group_id, $parent_id, $option_value, $counter, $is_default );
-							if ( ! $wpdb->query( $sql ) ) {
-								return false;
-							}
-						}
-
-						$counter++;
-					}
-				}
-			}
-
-			/**
-			 * Fires after the current field instance gets saved.
-			 *
-			 * @since BuddyPress (1.0.0)
-			 *
-			 * @param BP_XProfile_Field Current instance of the field being saved.
-			 */
-			do_action_ref_array( 'xprofile_field_after_save', array( $this ) );
-
-			// Recreate type_obj in case someone changed $this->type via a filter
-	 		$this->type_obj            = bp_xprofile_create_field_type( $this->type );
-	 		$this->type_obj->field_obj = $this;
-
-			return $field_id;
-		} else {
-			return false;
-		}
+		// Return the field ID
+		return (int) $this->id;
 	}
 
 	/**
@@ -404,36 +345,33 @@
 	 *
 	 * @since BuddyPress (1.2.0)
 	 *
-	 * @global object $wpdb
-	 *
 	 * @param  bool  $for_editing
 	 * @return array
 	 */
 	public function get_children( $for_editing = false ) {
-		global $wpdb;
 
 		// This is done here so we don't have problems with sql injection
-		if ( empty( $for_editing ) && ( 'asc' === $this->order_by ) ) {
-			$sort_sql = 'ORDER BY name ASC';
-		} elseif ( empty( $for_editing ) && ( 'desc' === $this->order_by ) ) {
-			$sort_sql = 'ORDER BY name DESC';
+		if ( ( false === $for_editing ) && in_array( strtoupper( $this->order_by ), array( 'ASC', 'DESC' ) ) ) {
+			$order_by = 'name';
+			$sort     = strtoupper( $this->order_by );
 		} else {
-			$sort_sql = 'ORDER BY option_order ASC';
+			$order_by = 'f.option_order';
+			$sort     = 'ASC';
 		}
 
-		// This eliminates a problem with getting all fields when there is no
-		// id for the object
-		if ( empty( $this->id ) ) {
-			$parent_id = -1;
-		} else {
-			$parent_id = $this->id;
+		// Get children
+		$children = self::get_fields( array(
+			'parent_id'         => $this->id,
+			'order_by'          => $order_by,
+			'sort'              => $sort,
+			'update_meta_cache' => false
+		) );
+
+		// Set children to false if empty or invalid
+		if ( empty( $children ) || ! is_array( $children ) ) {
+			$children = false;
 		}
 
-		$bp  = buddypress();
-		$sql = $wpdb->prepare( "SELECT * FROM {$bp->profile->table_name_fields} WHERE parent_id = %d AND group_id = %d {$sort_sql}", $parent_id, $this->group_id );
-
-		$children = $wpdb->get_results( $sql );
-
 		/**
 		 * Filters the found children for a field.
 		 *
@@ -446,6 +384,62 @@
 	}
 
 	/**
+	 * Delete a profile field
+	 *
+	 * @since BuddyPress (1.1.0)
+	 *
+	 * @global object  $wpdb
+	 * @param  boolean $delete_data
+	 * @param  boolean $delete_children
+	 * @return boolean
+	 */
+	public function delete( $delete_data = false, $delete_children = true ) {
+		global $wpdb;
+
+		// Prevent deletion if no ID is present
+		// Prevent deletion by url when can_delete is false.
+		if ( empty( $this->id ) || empty( $this->can_delete ) ) {
+			return false;
+		}
+
+		$bp = buddypress();
+
+		// Attempt to get fields to delete
+		$sql      = $wpdb->prepare( "SELECT id FROM {$bp->profile->table_name_fields} WHERE id = %d LIMIT 1", $this->id );
+		$field_id = $wpdb->get_var( $sql );
+
+		// Bail if no children exist
+		if ( empty( $field_id ) || is_wp_error( $field_id ) ) {
+			return false;
+		}
+
+		// Attempt to delete
+		$sql     = $wpdb->prepare( "DELETE FROM {$bp->profile->table_name_fields} WHERE id = %d LIMIT 1", $this->id );
+		$deleted = $wpdb->query( $sql );
+
+		// Bail if no children exist
+		if ( empty( $deleted ) || is_wp_error( $deleted ) ) {
+			return false;
+		}
+
+		// Delete cache
+		// Delete the relevant caches
+		$this->purge_caches();
+
+		// Maybe delete children
+		if ( true === $delete_children ) {
+			$this->delete_children();
+		}
+
+		// delete the data in the DB for this field
+		if ( true === $delete_data ) {
+			BP_XProfile_ProfileData::delete_for_field( $this->id );
+		}
+
+		return true;
+	}
+
+	/**
 	 * Delete all field children for this field
 	 *
 	 * @since BuddyPress (1.2.0)
@@ -453,34 +447,554 @@
 	 * @global object $wpdb
 	 */
 	public function delete_children() {
+
+		return BP_XProfile_Field::delete_fields( array(
+			'parent_id'   => $this->id,
+			'delete_data' => false
+		) );
+	}
+
+	/** Static Methods ********************************************************/
+
+	/**
+	 * Get profile fields, as specified by parameters
+	 *
+	 * @see WP_Meta_Query::queries for a description of the 'meta_query'
+	 *      parameter format.
+	 *
+	 * @param array $args {
+	 *     An array of arguments. All items are optional.
+
+	 * }
+	 */
+	public static function get_fields( $args = '' ) {
 		global $wpdb;
 
-		$bp  = buddypress();
-		$sql = $wpdb->prepare( "DELETE FROM {$bp->profile->table_name_fields} WHERE parent_id = %d", $this->id );
+		$r = bp_parse_args( $args, array(
+			'include'           => false,
+			'exclude'           => false,
+			'group_id'          => false,
+			'group_not_in'      => false,
+			'parent_id'         => false,
+			'parent_not_in'     => false,
+			'type_in'           => false,
+			'type_not_in'       => false,
+			'name'              => '',
+			'is_required'       => false,
+			'is_default_option' => false,
+			'can_delete'        => 1,
+			'order_by'          => false,
+			'sort'              => 'ASC',
+			'fetch_data'        => false,
+			'meta_query'        => false,
+			'update_meta_cache' => true
+		), 'xprofile_get_fields' );
 
-		$wpdb->query( $sql );
+		// Get the profile fields table name
+		$table_name = buddypress()->profile->table_name_fields;
+
+		// METADATA
+		$meta_query_sql = self::get_meta_query_sql( $r['meta_query'] );
+
+		// SELECT
+		$select_sql = "SELECT DISTINCT f.id";
+
+		// FROM
+		$from_sql   = " FROM {$table_name} f";
+
+		// JOIN
+		$join_sql   = $meta_query_sql['join'];
+
+		// WHERE
+		$where_sql  = self::get_where_sql( $r, $select_sql, $from_sql, $join_sql, $meta_query_sql );
+
+		// Bail if no `WHERE` conditions
+		if ( empty( $where_sql ) ) {
+			return array();
+		}
+
+		/** Order & Sort ******************************************************/
+
+		// Sorting
+		$sort = $r['sort'];
+		if ( ! in_array( $sort, array( 'ASC', 'DESC' ) ) ) {
+			$sort = 'ASC';
+		}
+
+		// Ordering
+		$order_by = 'f.field_order';
+		if ( ! empty( $r['order_by'] ) ) {
+			$order_by = $r['order_by'];
+		}
+
+		/** Index *************************************************************/
+
+		// Get the query index, if used
+		$index_hint_sql = self::get_index_hint_sql( $where_sql );
+
+		/** Query *************************************************************/
+
+		// Query first for profile fields
+		$field_ids_sql = "{$select_sql} {$from_sql} {$index_hint_sql} {$join_sql} {$where_sql} ORDER BY {$order_by} {$sort}";
+		$field_ids     = $wpdb->get_col( $field_ids_sql );
+
+		// Bail if no field IDs
+		if ( empty( $field_ids ) ) {
+			return array();
+		}
+
+		// Get field data for all field IDs - handles cache analysis
+		$fields = self::get_fields_data( $field_ids, $r['fetch_data'] );
+
+		// Maybe fetch user data for all queried fields
+		if ( ! empty( $r['fetch_data'] ) ) {
+			$fields = self::get_data_for_field_ids( $field_ids, $r['fetch_data'] );
+		}
+
+		// Update caches
+		if ( ! empty( $r['update_meta_cache'] ) ) {
+			bp_xprofile_update_meta_cache( array( 'field' => $field_ids ) );
+		}
+
+		return $fields;
 	}
 
-	/** Static Methods ********************************************************/
+	/**
+	 * Delete profile fields, as specified by parameters
+	 *
+	 * @see BP_Activity_Activity::get_filter_sql() for a description of the
+	 *      'filter' parameter.
+	 * @see WP_Meta_Query::queries for a description of the 'meta_query'
+	 *      parameter format.
+	 *
+	 * @param array $args {
+	 *     An array of arguments. All items are optional.
+	 *
+	 * }
+	 * @return array The array returned has two keys:
+	 *     - 'total' is the count of located activities
+	 *     - 'activities' is an array of the located activities
+	 */
+	public static function delete_fields( $args = '' ) {
+		global $wpdb;
 
-	public static function get_type( $field_id = 0 ) {
+		$r = wp_parse_args( $args, array(
+			'include'           => false,
+			'exclude'           => false,
+			'group_id'          => false,
+			'group_not_in'      => false,
+			'parent_id'         => false,
+			'parent_not_in'     => false,
+			'type_in'           => false,
+			'type_not_in'       => false,
+			'name'              => '',
+			'is_required'       => false,
+			'is_default_option' => false,
+			'delete_data'       => false,
+			'meta_query'        => false,
+			'update_meta_cache' => true
+		) );
+
+		// Get the profile fields table name
+		$table_name = buddypress()->profile->table_name_fields;
+
+		// METADATA
+		$meta_query_sql = self::get_meta_query_sql( $r['meta_query'] );
+
+		// SELECT
+		$select_sql = "SELECT *";
+
+		// FROM
+		$from_sql   = " FROM {$table_name} f";
+
+		// JOIN
+		$join_sql   = $meta_query_sql['join'];
+
+		// WHERE
+		$where_sql  = self::get_where_sql( $r, $select_sql, $from_sql, $join_sql, $meta_query_sql );
+
+		// Bail if no `WHERE` conditions
+		if ( empty( $where_sql ) ) {
+			return false;
+		}
+
+		// Fetch all activities being deleted so we can perform more actions
+		$fields = $wpdb->get_results( "SELECT {$from_sql} {$join_sql} {$where_sql}" );
+
+		/**
+		 * Action to allow intercepting xprofile fields to be deleted
+		 *
+		 * @since BuddyPress (2.3.0)
+		 *
+		 * @param array $activities Array of fields
+		 * @param array $r          Array of parsed arguments
+		 */
+		do_action_ref_array( 'bp_xprofile_fields_before_delete', array( $fields, $r ) );
+
+		/** Query *************************************************************/
+
+		// Query first for profile fields
+		// Attempt to delete activities from the database
+		$delete_ids_sql = "DELETE {$from_sql} {$join_sql} {$where_sql}";
+		$deleted = $wpdb->query( $delete_ids_sql );
+
+		// Bail if nothing was deleted
+		if ( empty( $deleted ) ) {
+			return false;
+		}
+
+		/**
+		 * Action to allow intercepting profile fields just deleted
+		 *
+		 * @since BuddyPress (2.3.0)
+		 *
+		 * @param array $activities Array of fields
+		 * @param array $r          Array of parsed arguments
+		 */
+		do_action_ref_array( 'bp_xprofile_fields_after_delete', array( $fields, $r ) );
+
+		// Pluck the activity IDs out of the $activities array
+		$field_ids = wp_parse_id_list( wp_list_pluck( $fields, 'id' ) );
+
+		// Handle accompanying field options and meta deletion
+		if ( ! empty( $field_ids ) ) {
+
+			// Delete all profile field meta entries
+			//BP_XProfile_Field::delete_field_meta_entries( $field_ids );
+
+			// Setup empty array for comments
+			$option_ids = array();
+
+			// Loop through activity ids and attempt to delete comments
+			foreach ( $field_ids as $field_id ) {
+
+				// Attempt to delete comments
+				$option_ids = BP_XProfile_Field::delete_fields( array(
+					'parent_id'   => $field_id,
+					'delete_data' => $r['delete_data']
+				) );
+
+				// Merge IDs together
+				if ( ! empty( $option_ids ) ) {
+					$option_ids = array_merge( $field_ids, $option_ids );
+				}
+			}
+
+			// Merge activity IDs with any deleted comment IDs
+			if ( ! empty( $option_ids ) ) {
+				$field_ids = array_unique( array_merge( $field_ids, $option_ids ) );
+			}
+		}
+
+		// Maybe fetch user data for all queried fields
+		if ( ! empty( $r['delete_data'] ) ) {
+			//$fields = self::delete_data_for_field_ids( $field_ids );
+		}
+
+		// Update caches
+		if ( ! empty( $r['update_meta_cache'] ) ) {
+			bp_xprofile_update_meta_cache( array( 'field' => $field_ids ) );
+		}
+
+		return true;
+	}
+
+	/**
+	 * Get the `WHERE` part of the MySQL query for profile fields
+	 *
+	 * @since BuddyPress (2.3.0)
+	 *
+	 * @global object $wpdb
+	 * @param  array  $r
+	 * @param  string $select_sql
+	 * @param  string $from_sql
+	 * @param  string $join_sql
+	 * @param  string $meta_query_sql
+	 *
+	 * @return mixed
+	 */
+	private static function get_where_sql( $r = array(), $select_sql = '', $from_sql = '', $join_sql = '', $meta_query_sql = '' ) {
 		global $wpdb;
 
+		// Setup empty array for `WHERE` conditions
+		$where_conditions = array();
+
+		/** Field ID **********************************************************/
+
+		// Setup IN query for field IDs
+		if ( ! empty( $r['include'] ) ) {
+			$include_in                  = implode( ',', wp_parse_id_list( $r['include'] ) );
+			$where_conditions['include'] = "id IN ({$include_in})";
+		}
+
+		// Setup NOT IN query for field IDs
+		if ( ! empty( $r['exclude'] ) ) {
+			$exclude_not_in              = implode( ',', wp_parse_id_list( $r['exclude'] ) );
+			$where_conditions['exclude'] = "id NOT IN ({$exclude_not_in})";
+		}
+
+		/** Group ID **********************************************************/
+
+		// Setup IN query for field-group IDs
+		if ( ! empty( $r['group_id'] ) ) {
+			$group_in                     = implode( ',', wp_parse_id_list( $r['group_id'] ) );
+			$where_conditions['group_in'] = "group_id IN ({$group_in})";
+		}
+
+		// Setup NOT IN query for field-group IDs
+		if ( ! empty( $r['group_not_in'] ) ) {
+			$group_not_in                     = implode( ',', wp_parse_id_list( $r['group_not_in'] ) );
+			$where_conditions['group_not_in'] = "group_id NOT IN ({$group_not_in})";
+		}
+
+		/** Parent ID *********************************************************/
+
+		// Setup IN query for parent-field IDs
+		if ( ! empty( $r['parent_id'] ) || ( '0' === $r['parent_id'] ) ) {
+			$parent_in                     = implode( ',', wp_parse_id_list( $r['parent_id'] ) );
+			$where_conditions['parent_in'] = "parent_id IN ({$parent_in})";
+		}
+
+		// Setup NOT IN query for parent-field IDs
+		if ( ! empty( $r['parent_not_in'] ) ) {
+			$parent_not_in                     = implode( ',', wp_parse_id_list( $r['parent_not_in'] ) );
+			$where_conditions['parent_not_in'] = "parent_id NOT IN ({$parent_not_in})";
+		}
+
+		/** Types *************************************************************/
+
+		// Setup IN query for type-field IDs
+		if ( ! empty( $r['type_in'] ) ) {
+			$type_in                     = implode( ',', wp_parse_id_list( $r['type_in'] ) );
+			$where_conditions['type_in'] = "type IN ({$type_in})";
+		}
+
+		// Setup NOT IN query for type-field IDs
+		if ( ! empty( $r['type_not_in'] ) ) {
+			$type_not_in                     = implode( ',', wp_parse_id_list( $r['type_not_in'] ) );
+			$where_conditions['type_not_in'] = "type NOT IN ({$type_not_in})";
+		}
+
+		/** Name **************************************************************/
+
+		// Setup IN query for type-field IDs
+		if ( ! empty( $r['name'] ) ) {
+			$where_conditions['name'] = $wpdb->prepare( "name = %s", $r['name'] );
+		}
+
+		/** Meta **************************************************************/
+
+		// meta query
+		if ( ! empty( $meta_query_sql['where'] ) ) {
+			$where_conditions['meta_query'] = $meta_query_sql['where'];
+		}
+
+		/**
+		 * Filters the MySQL WHERE conditions for the XProfile field get method.
+		 *
+		 * @since BuddyPress (2.3.0)
+		 *
+		 * @param array  $where_conditions Current conditions for MySQL WHERE statement.
+		 * @param array  $r                Parsed arguments passed into method.
+		 * @param string $select_sql       Current SELECT MySQL statement at point of execution.
+		 * @param string $from_sql         Current FROM MySQL statement at point of execution.
+		 * @param string $join_sql         Current INNER JOIN MySQL statement at point of execution.
+		 */
+		$where_conditions = apply_filters( 'bp_xprofile_get_where_conditions', $where_conditions, $r, $select_sql, $from_sql, $join_sql, $meta_query_sql );
+
+		// Return known WHERE conditions
+		if ( ! empty( $where_conditions ) ) {
+			return 'WHERE ' . join( ' AND ', $where_conditions );
+		}
+
+		// No where conditions
+		return false;
+	}
+
+	/**
+	 * Get the `USE INDEX` part of the profile field MySQL query
+	 *
+	 * @since BuddyPress (2.3.0)
+	 *
+	 * @return string
+	 */
+	private static function get_index_hint_sql( $where_sql = '' ) {
+
+		/**
+		 * Filters the preferred order of indexes for xprofile fields.
+		 *
+		 * @since BuddyPress (2.3.0)
+		 *
+		 * @param array Array of indexes in preferred order.
+		 */
+		$indexes = apply_filters( 'bp_xprofile_preferred_index_order', array( 'group_id', 'parent_id', 'field_order', 'can_delete', 'is_required' ) );
+
+		// Loop through possible indices and pick the first one. Not a very
+		// precise indication, but currently does the job okay.
+		foreach ( $indexes as $index ) {
+			if ( false !== strpos( $where_sql, $index ) ) {
+				$the_index = $index;
+				break; // Take the first one we find
+			}
+		}
+
+		// Return known USE INDEX condition
+		if ( ! empty( $the_index ) ) {
+			return "USE INDEX ({$the_index})";
+		}
+
+		// No USE INDEX condition
+		return '';
+	}
+
+	/**
+	 * Get the SQL for the 'meta_query' param in BP_XProfile_Field::get_fields().
+	 *
+	 * We use WP_Meta_Query to do the heavy lifting of parsing the
+	 * meta_query array and creating the necessary SQL clauses. However,
+	 * since BP_XProfile_Field::get_fields() builds its SQL differently than
+	 * WP_Query, we have to alter the return value (stripping the leading
+	 * AND keyword from the 'where' clause).
+	 *
+	 * @since BuddyPress (2.3.0)
+	 *
+	 * @param array $meta_query An array of meta_query filters. See the
+	 *                          documentation for WP_Meta_Query for details.
+	 *
+	 * @return array $sql_array 'join' and 'where' clauses.
+	 */
+	private static function get_meta_query_sql( $meta_query = array() ) {
+		global $wpdb;
+
+		// Default meta sql
+		$sql_array = array(
+			'join'  => '',
+			'where' => '',
+		);
+
+		// Attempt to query using metadata
+		if ( ! empty( $meta_query ) ) {
+			$fields_meta_query = new BP_XProfile_Meta_Query( $meta_query );
+
+			// WP_Meta_Query expects the table name at $wpdb->xprofile_fieldmeta
+			$wpdb->xprofile_fieldmeta = buddypress()->profile->table_name_meta;
+
+			// Attempt to get meta part of query
+			$meta_sql = $fields_meta_query->get_sql( 'xprofile_field', 'f', 'id' );
+
+			// Strip the leading AND - handled it in get_fields()
+			$sql_array['where'] = preg_replace( '/^\sAND/', '', $meta_sql['where'] );
+			$sql_array['join']  = $meta_sql['join'];
+		}
+
+		return $sql_array;
+	}
+
+	/**
+	 * Get field-data for select field IDs for a specific user ID
+	 *
+	 * @since BuddyPress (2.3.0)
+	 *
+	 * @param array $field_ids
+	 * @param int   $user_id
+	 *
+	 * @return array
+	 */
+	private static function get_data_for_field_ids( $field_ids = array(), $user_id = 0 ) {
+
+		// Setup the fields array
+		$fields = array();
+
+		// Bail if no field IDs or user ID
+		if ( empty( $field_ids ) || empty( $user_id ) ) {
+			return $fields;
+		}
+
+		// Loop through fields, and get data for the user ID
+		foreach ( $field_ids as $field_id ) {
+			$field       = new BP_XProfile_Field( $field_id );
+			$field->data = $field->get_field_data( $user_id );
+			$fields[]    = $field;
+		}
+
+		// Return the fields and their data for the user ID
+		return $fields;
+	}
+
+	/**
+	 * Convert field IDs to field objects, as expected in template loop.
+	 *
+	 * @since BuddyPress (2.3.0)
+	 *
+	 * @param  array $field_ids Array of field IDs
+	 *
+	 * @return array
+	 */
+	private static function get_fields_data( $field_ids = array() ) {
+		global $wpdb;
+
+		// Declare empty fields array
+		$fields = array();
+
+		// Bail if no field ID's passed
+		if ( empty( $field_ids ) ) {
+			return $fields;
+		}
+
+		// Determine if any queried field ID's are uncached
+		$uncached_ids = bp_get_non_cached_ids( $field_ids, 'bp_xprofile_fields' );
+
+		// Prime caches as necessary
+		if ( ! empty( $uncached_ids ) ) {
+
+			// Format the field ID's for use in the query below
+			$uncached_ids_sql = implode( ',', wp_parse_id_list( $uncached_ids ) );
+
+			// Fetch data from field table, preserving order
+			$table_name    = buddypress()->profile->table_name_fields;
+			$sql           = "SELECT * FROM {$table_name} WHERE id IN ({$uncached_ids_sql})";
+			$queried_fdata = $wpdb->get_results( $sql );
+
+			// Cache the queried results
+			foreach ( (array) $queried_fdata as $fdata ) {
+				wp_cache_set( $fdata->id, $fdata, 'bp_xprofile_fields' );
+			}
+		}
+
+		// Get all field data from the (now primed) cache
+		foreach ( $field_ids as $field_id ) {
+			$fields[] = wp_cache_get( $field_id, 'bp_xprofile_fields' );
+		}
+
+		// Return newly cached field IDs
+		return $fields;
+	}
+
+	/**
+	 * Get the type for a given field ID
+	 *
+	 * @since BuddyPress (1.1.0)
+	 *
+	 * @param  int $field_id
+	 *
+	 * @return boolean
+	 */
+	public static function get_type( $field_id = 0 ) {
+
 		// Bail if no field ID
 		if ( empty( $field_id ) ) {
 			return false;
 		}
 
-		$bp   = buddypress();
-		$sql  = $wpdb->prepare( "SELECT type FROM {$bp->profile->table_name_fields} WHERE id = %d", $field_id );
-		$type = $wpdb->get_var( $sql );
+		// Attempt to get the field
+		$field = new BP_XProfile_Field( $field_id );
 
-		// Return field type
-		if ( ! empty( $type ) ) {
-			return $type;
+		// Bail if no field found
+		if ( empty( $field->type ) ) {
+			return false;
 		}
 
-		return false;
+		return $field->type;
 	}
 
 	/**
@@ -495,17 +1009,17 @@
 	 * @return boolean
 	 */
 	public static function delete_for_group( $group_id = 0 ) {
-		global $wpdb;
 
 		// Bail if no group ID
 		if ( empty( $group_id ) ) {
 			return false;
 		}
 
-		$bp      = buddypress();
-		$sql     = $wpdb->prepare( "DELETE FROM {$bp->profile->table_name_fields} WHERE group_id = %d", $group_id );
-		$deleted = $wpdb->get_var( $sql );
-
+		// Attempt to delete fields
+		$deleted = self::delete_fields( array(
+			'group_id' => $group_id
+		) ); 
+		
 		// Return true if fields were deleted
 		if ( false !== $deleted ) {
 			return true;
@@ -519,23 +1033,33 @@
 	 *
 	 * @since BuddyPress (1.5.0)
 	 *
-	 * @global object $wpdb
-	 * @param  string $field_name
+	 * @param string $field_name
 	 *
 	 * @return boolean
 	 */
 	public static function get_id_from_name( $field_name = '' ) {
-		global $wpdb;
 
-		$bp = buddypress();
+		// Bail if no field ID
+		if ( empty( $field_name ) ) {
+			return false;
+		}
 
-		if ( empty( $bp->profile->table_name_fields ) || empty( $field_name ) ) {
+		// Attempt to get the field
+		$fields = self::get_fields( array(
+			'name'      => $field_name,
+			'parent_id' => '0'
+		) );
+
+		// Return false if not found
+		if ( empty( $fields ) ) {
 			return false;
 		}
 
-		$sql = $wpdb->prepare( "SELECT id FROM {$bp->profile->table_name_fields} WHERE name = %s AND parent_id = 0", $field_name );
+		// Take the first field
+		$field = reset( $fields );
 
-		return $wpdb->get_var( $sql );
+		// Return field ID if found
+		return $field->id;
 	}
 
 	/**
@@ -543,8 +1067,6 @@
 	 *
 	 * @since BuddyPress (1.5.0)
 	 *
-	 * @global object $wpdb
-	 *
 	 * @param  int $field_id
 	 * @param  int $position
 	 * @param  int $field_group_id
@@ -552,7 +1074,6 @@
 	 * @return boolean
 	 */
 	public static function update_position( $field_id, $position = null, $field_group_id = null ) {
-		global $wpdb;
 
 		// Bail if invalid position or field group
 		if ( ! is_numeric( $position ) || ! is_numeric( $field_group_id ) ) {
@@ -560,21 +1081,31 @@
 		}
 
 		// Get table name and field parent
-		$table_name = buddypress()->profile->table_name_fields;
-		$sql        = $wpdb->prepare( "UPDATE {$table_name} SET field_order = %d, group_id = %d WHERE id = %d", $position, $field_group_id, $field_id );
-		$parent     = $wpdb->query( $sql );
+		$field              = new BP_XProfile_Field( $field_id );
+		$field->field_order = $position;
+		$field->group_id    = $field_group_id;
 
-		// Update $field_id with new $position and $field_group_id
-		if ( ! empty( $parent ) && ! is_wp_error( $parent ) ) {
+		// Bail if field did not save - pass `true` to prevent option deletion
+		if ( ! $field->save() ) {
+			return false;
+		}
 
-			// Update any children of this $field_id
-			$sql = $wpdb->prepare( "UPDATE {$table_name} SET group_id = %d WHERE parent_id = %d", $field_group_id, $field_id );
-			$wpdb->query( $sql );
+		// Regather the field and its children
+		$field    = new BP_XProfile_Field( $field_id );
+		$children = $field->get_children();
 
-			return $parent;
+		// If child fields exist, update them
+		if ( ! empty( $children ) ) {
+
+			// Loop through children and update field group ID
+			foreach ( $children as $option_field ) {
+				$option           = new BP_XProfile_Field( $option_field->id );
+				$option->group_id = $field->group_id;
+				$option->save();
+			}
 		}
 
-		return false;
+		return true;
 	}
 
 	/**
@@ -607,16 +1138,38 @@
 	}
 
 	/**
-	 * This function populates the items for radio buttons checkboxes and drop
-	 * down boxes.
+	 * Check if a field option has a default value
+	 *
+	 * This utility function is used by `update_field_options` and checks if a
+	 * field-option should be checked or selected by default. Because different
+	 * field types $_POST different results, we check for both array and integer
+	 * values and compare between the two.
+	 *
+	 * @since BuddyPress (2.3.0)
+	 *
+	 * @param  mixed $defaults
+	 * @param  mixed $option_key
+	 *
+	 * @return int
 	 */
-	public function render_admin_form_children() {
-		foreach ( array_keys( bp_xprofile_get_field_types() ) as $field_type ) {
-			$type_obj = bp_xprofile_create_field_type( $field_type );
-			$type_obj->admin_new_field_html( $this );
+	private static function is_default_field_option( $defaults = '', $option_key = '' ) {
+		$is_default = 0;
+
+		// Check for multiple default values
+		if ( is_array( $defaults ) && isset( $defaults[ $option_key ] ) ) {
+			$is_default = 1;
+
+		// Check for single default value
+		} elseif ( $defaults === $option_key ) {
+			$is_default = 1;
 		}
+
+		// Cast as int and return the result
+		return (int) $is_default;
 	}
 
+	/** Public Methods ********************************************************/
+
 	/**
 	 * Oupput the admin form for this field
 	 *
@@ -625,6 +1178,8 @@
 	 * @param type $message
 	 */
 	public function render_admin_form( $message = '' ) {
+
+		// Adding a new field
 		if ( empty( $this->id ) ) {
 			$title  = __( 'Add New Field', 'buddypress' );
 			$action	= "users.php?page=bp-profile-setup&amp;group_id=" . $this->group_id . "&amp;mode=add_field#tabs-" . $this->group_id;
@@ -635,9 +1190,11 @@
 				$this->description = $_POST['description'];
 				$this->is_required = $_POST['required'];
 				$this->type        = $_POST['fieldtype'];
-				$this->order_by    = $_POST["sort_order_{$this->type}"];
+				$this->order_by    = ! empty( $_POST["sort_order_{$this->type}"] ) ? $_POST["sort_order_{$this->type}"] : '';
 				$this->field_order = $_POST['field_order'];
 			}
+
+		// Editing an existing field
 		} else {
 			$title  = __( 'Edit Field', 'buddypress' );
 			$action = "users.php?page=bp-profile-setup&amp;mode=edit_field&amp;group_id=" . $this->group_id . "&amp;field_id=" . $this->id . "#tabs-" . $this->group_id;
@@ -680,9 +1237,12 @@
 							// Output the required metabox
 							$this->required_metabox();
 
-							// Output the field visibility metaboxes
+							// Output the field visibility metabox
 							$this->visibility_metabox();
 
+							// Output the signup metabox
+							$this->signup_metabox();
+
 							/**
 							 * Fires after XProfile Field sidebar metabox.
 							 *
@@ -731,6 +1291,8 @@
 	<?php
 	}
 
+	/** Private Methods *******************************************************/
+
 	/**
 	 * Private method used to display the submit metabox
 	 *
@@ -902,6 +1464,29 @@
 	}
 
 	/**
+	 * Output the metabox for enabling this field to appear on user registration
+	 *
+	 * @since BuddyPress (2.3.0)
+	 */
+	private function signup_metabox() {
+	?>
+
+		<div class="postbox">
+			<h3><label for="signup-field"><?php _e( 'Sign Ups', 'buddypress' ); ?></label></h3>
+			<div class="inside">
+				<ul>
+					<li>
+						<input type="checkbox" id="signup-position" name="signup-position" value="1" <?php checked( $this->signup_position, ! null ); ?> />
+						<label for="signup-position"><?php esc_html_e( 'Display on Registration', 'buddypress' ); ?></label>
+					</li>
+				</ul>
+			</div>
+		</div>
+
+	<?php
+	}
+
+	/**
 	 * Output the metabox for setting what type of field this is
 	 *
 	 * @since BuddyPress (2.3.0)
@@ -938,6 +1523,19 @@
 	}
 
 	/**
+	 * This function populates the items for radio buttons checkboxes and drop
+	 * down boxes.
+	 *
+	 * @since BuddyPress (1.1.0)
+	 */
+	private function render_admin_form_children() {
+		foreach ( array_keys( bp_xprofile_get_field_types() ) as $field_type ) {
+			$type_obj = bp_xprofile_create_field_type( $field_type );
+			$type_obj->admin_new_field_html( $this );
+		}
+	}
+
+	/**
 	 * Output hidden fields used by default field
 	 *
 	 * @since BuddyPress (2.3.0)
@@ -975,4 +1573,113 @@
 		// Compare & return
 		return (bool) ( 1 === (int) $field_id );
 	}
+
+	/**
+	 * Purge caches relevant to field updates
+	 *
+	 * @since BuddyPress (2.3.0)
+	 */
+	private function purge_caches() {
+
+		// Delete the cached value for this field
+		wp_cache_delete( $this->id, 'bp_xprofile_fields' );
+
+		// Bust cache of parent field
+		if ( ! empty( $this->parent_id ) ) {
+			wp_cache_delete( $this->parent_id, 'bp_xprofile_fields' );
+		}
+
+		// Bust cache of parent group
+		if ( ! empty( $this->group_id ) ) {
+			wp_cache_delete( 'all',           'bp_xprofile_groups' );
+			wp_cache_delete( $this->group_id, 'bp_xprofile_groups' );
+		}
+	}
+
+	/**
+	 * Update fields-options when a field is saved
+	 *
+	 * If a field type supports field-options, BuddyPress will resave all of
+	 * them to ensure the integrity of their data. This is necessary and
+	 * possible because field-data for each user is stored keyed to the parent
+	 * field ID, and not to the child field-option ID.
+	 *
+	 * @since BuddyPress (2.3.0)
+	 *
+	 * @param type $updating_position
+	 */
+	private function update_field_options() {
+
+		/**
+		 * Check to see if this is a field with child options, and that field
+		 * options are being passed in the $_POST request.
+		 */
+		if ( ! empty( $this->type_obj->supports_options ) && ( isset( $_POST["{$this->type}_option"] ) || isset( $_POST["isDefault_{$this->type}_option"] ) ) ) {
+
+			/**
+			 * Remove any field-options for this field. They will be re-added if
+			 * needed. This prenets orphaned options if the user changes a field
+			 * from a radio-button with options to a textbox without.
+			 *
+			 * It's maybe a bit of a brute-force approach, but seems easier than
+			 * querying for items & updating/deleting/creating each time;
+			 * and 60% of the time, it works everytime, so that's nice.
+			 */
+			$this->delete_children();
+
+			// Allow plugins to filter the field's child options (i.e. the items in a selectbox).
+			$post_options  = ! empty( $_POST["{$this->type}_option"]           ) ? $_POST["{$this->type}_option"]           : '';
+			$post_defaults = ! empty( $_POST["isDefault_{$this->type}_option"] ) ? $_POST["isDefault_{$this->type}_option"] : '';
+
+			/**
+			 * Filters the submitted field option value before saved.
+			 *
+			 * @since BuddyPress (1.5.0)
+			 *
+			 * @param string            $post_options Submitted option value.
+			 * @param BP_XProfile_Field $type         Current field type being saved for.
+			 */
+			$options = apply_filters( 'xprofile_field_options_before_save', $post_options,  $this->type );
+
+			/**
+			 * Filters the default field option value before saved.
+			 *
+			 * @since BuddyPress (1.5.0)
+			 *
+			 * @param string            $post_defaults Default option value.
+			 * @param BP_XProfile_Field $type          Current field type being saved for.
+			 */
+			$defaults = apply_filters( 'xprofile_field_default_before_save', $post_defaults, $this->type );
+
+			// There are options that need recreating
+			if ( ! empty( $options ) ) {
+
+				// Start counter at 1 to avoid 0 values
+				$counter = 1;
+
+				// Loop through options and re-create them
+				foreach ( $options as $option_key => $option_name ) {
+
+					// Is this field a default option?
+					$is_default = self::is_default_field_option( $defaults, $option_key );
+
+					// Recreate the option based on value and other criteria
+					if ( ! empty( $option_name ) ) {
+						$option                    = new BP_XProfile_Field();
+						$option->group_id          = $this->group_id;
+						$option->parent_id         = $this->id;
+						$option->type              = 'option';
+						$option->name              = $option_name;
+						$option->description       = '';
+						$option->is_required       = 0;
+						$option->is_default_option = $is_default;
+						$option->option_order      = $counter;
+						$option->save();
+					}
+
+					$counter++;
+				}
+			}
+		}
+	}
 }
Index: src/bp-xprofile/classes/class-bp-xprofile-group.php
--- src/bp-xprofile/classes/class-bp-xprofile-group.php
+++ src/bp-xprofile/classes/class-bp-xprofile-group.php
@@ -301,6 +301,8 @@
 			return $groups;
 		}
 
+		/** Fields ************************************************************/
+
 		// Get the group ids from the groups we found
 		$group_ids = wp_list_pluck( $groups, 'id' );
 
@@ -312,27 +314,25 @@
 			return $groups;
 		}
 
-		// Setup IN query from group IDs
-		$group_ids_in = implode( ',', (array) $group_ids );
-
 		// Support arrays and comma-separated strings
 		$exclude_fields_cs = wp_parse_id_list( $r['exclude_fields'] );
 
 		// Visibility - Handled here so as not to be overridden by sloppy use of the
 		// exclude_fields parameter. See bp_xprofile_get_hidden_fields_for_user()
 		$hidden_user_fields = bp_xprofile_get_hidden_fields_for_user( $r['user_id'] );
-		$exclude_fields_cs  = array_merge( $exclude_fields_cs, $hidden_user_fields );
-		$exclude_fields_cs  = implode( ',', $exclude_fields_cs );
+		$exclude_field_ids  = array_filter( array_merge( $exclude_fields_cs, $hidden_user_fields ) );
 
-		// Setup IN query for field IDs
-		if ( ! empty( $exclude_fields_cs ) ) {
-			$exclude_fields_sql = "AND id NOT IN ({$exclude_fields_cs})";
-		} else {
-			$exclude_fields_sql = '';
-		}
-
+		// Setup ::get() arguments
 		// Fetch the fields
-		$fields    = $wpdb->get_results( "SELECT id, name, description, type, group_id, is_required FROM {$bp->profile->table_name_fields} WHERE group_id IN ( {$group_ids_in} ) AND parent_id = 0 {$exclude_fields_sql} ORDER BY field_order" );
+		$fields = BP_XProfile_Field::get_fields( array(
+			'exclude'           => $exclude_field_ids,
+			'group_id'          => $group_ids,
+			'parent_id'         => '0',
+			'update_meta_cache' => false, // for now
+			'fetch_data'        => false  // for now
+		) );
+
+		// Pluck the ID's 
 		$field_ids = wp_list_pluck( $fields, 'id' );
 
 		// Store field IDs for meta cache priming
@@ -343,6 +343,8 @@
 			return $groups;
 		}
 
+		/** Field Data ********************************************************/
+
 		// Maybe fetch field data
 		if ( ! empty( $r['fetch_field_data'] ) ) {
 
@@ -361,7 +363,7 @@
 					$maybe_value = maybe_unserialize( $data->value );
 
 					// Valid field values of 0 or '0' get caught by empty(), so we have an extra check for these. See #BP5731
-					if ( ( ! empty( $maybe_value ) || '0' == $maybe_value ) && false !== $key = array_search( $data->field_id, $field_ids ) ) {
+					if ( ( ! empty( $maybe_value ) || '0' === $maybe_value ) && false !== $key = array_search( $data->field_id, $field_ids ) ) {
 
 						// Fields that have data get removed from the list
 						unset( $field_ids[ $key ] );
@@ -369,7 +371,7 @@
 				}
 
 				// The remaining members of $field_ids are empty. Remove them.
-				foreach( $fields as $field_key => $field ) {
+				foreach ( $fields as $field_key => $field ) {
 					if ( in_array( $field->id, $field_ids ) ) {
 						unset( $fields[ $field_key ] );
 					}
Index: src/bp-xprofile/classes/class-bp-xprofile-meta-query.php
--- src/bp-xprofile/classes/class-bp-xprofile-meta-query.php No Base Revision
+++ src/bp-xprofile/classes/class-bp-xprofile-meta-query.php Locally New
@@ -0,0 +1,358 @@
+<?php
+
+/**
+ * Class for generating SQL clauses that filter a primary query according to
+ * XProfile metadata keys and values.
+ *
+ * `BP_XProfile_Meta_Query` is a helper that allows primary query classes, such
+ * as {@see WP_Query} and {@see WP_User_Query}, to filter their results by object
+ * metadata, by generating `JOIN` and `WHERE` subclauses to be attached
+ * to the primary SQL query string.
+ *
+ * @since BuddyPress (2.3.0)
+ */
+class BP_XProfile_Meta_Query extends WP_Meta_Query {
+
+	/**
+	 * Determine whether a query clause is first-order.
+	 *
+	 * A first-order meta query clause is one that has either a 'key', 'value',
+	 * or 'object' array key.
+	 *
+	 * @since BuddyPress (2.3.0)
+	 * @access protected
+	 *
+	 * @param array $query Meta query arguments.
+	 * @return bool Whether the query clause is a first-order clause.
+	 */
+	protected function is_first_order_clause( $query ) {
+		return isset( $query['key'] ) || isset( $query['value'] ) || isset( $query['object'] );
+	}
+
+	/**
+	 * Constructs a meta query based on 'meta_*' query vars
+	 *
+	 * @since BuddyPress (2.3.0)
+	 * @access public
+	 *
+	 * @param array $qv The query variables
+	 */
+	public function parse_query_vars( $qv ) {
+		$meta_query = array();
+
+		/*
+		 * For orderby=meta_value to work correctly, simple query needs to be
+		 * first (so that its table join is against an unaliased meta table) and
+		 * needs to be its own clause (so it doesn't interfere with the logic of
+		 * the rest of the meta_query).
+		 */
+		$primary_meta_query = array();
+		foreach ( array( 'key', 'compare', 'type' ) as $key ) {
+			if ( ! empty( $qv[ "meta_$key" ] ) ) {
+				$primary_meta_query[ $key ] = $qv[ "meta_$key" ];
+			}
+		}
+
+		// WP_Query sets 'meta_value' = '' by default.
+		if ( isset( $qv['meta_value'] ) && ( '' !== $qv['meta_value'] ) && ( ! is_array( $qv['meta_value'] ) || $qv['meta_value'] ) ) {
+			$primary_meta_query['value'] = $qv['meta_value'];
+		}
+
+		// BP_XProfile_Query sets 'object_type' = '' by default
+		if ( isset( $qv[ 'object_type' ] ) && ( '' !== $qv[ 'object_type' ] ) && ( ! is_array( $qv[ 'object_type' ] ) || $qv[ 'object_type' ] ) ) {
+			$meta_query[0]['object'] = $qv[ 'object_type' ];
+		}
+
+		$existing_meta_query = isset( $qv['meta_query'] ) && is_array( $qv['meta_query'] ) ? $qv['meta_query'] : array();
+
+		if ( ! empty( $primary_meta_query ) && ! empty( $existing_meta_query ) ) {
+			$meta_query = array(
+				'relation' => 'AND',
+				$primary_meta_query,
+				$existing_meta_query,
+			);
+		} elseif ( ! empty( $primary_meta_query ) ) {
+			$meta_query = array(
+				$primary_meta_query,
+			);
+		} elseif ( ! empty( $existing_meta_query ) ) {
+			$meta_query = $existing_meta_query;
+		}
+
+		$this->__construct( $meta_query );
+	}
+
+	/**
+	 * Generates SQL clauses to be appended to a main query.
+	 *
+	 * @since BuddyPress (2.3.0)
+	 * @access public
+	 *
+	 * @param string $type              Type of meta, eg 'user', 'post'.
+	 * @param string $primary_table     Database table where the object being filtered is stored (eg wp_users).
+	 * @param string $primary_id_column ID column for the filtered object in $primary_table.
+	 * @param object $context           Optional. The main query object.
+	 * @return array {
+	 *     Array containing JOIN and WHERE SQL clauses to append to the main query.
+	 *
+	 *     @type string $join  SQL fragment to append to the main JOIN clause.
+	 *     @type string $where SQL fragment to append to the main WHERE clause.
+	 * }
+	 */
+	public function get_sql( $type, $primary_table, $primary_id_column, $context = null ) {
+		if ( ! $meta_table = _get_meta_table( $type ) ) {
+			return false;
+		}
+
+		$this->meta_table     = $meta_table;
+		$this->meta_id_column = 'object_id';
+
+		$this->primary_table     = $primary_table;
+		$this->primary_id_column = $primary_id_column;
+
+		$sql = $this->get_sql_clauses();
+
+		/*
+		 * If any JOINs are LEFT JOINs (as in the case of NOT EXISTS), then all JOINs should
+		 * be LEFT. Otherwise posts with no metadata will be excluded from results.
+		 */
+		if ( false !== strpos( $sql['join'], 'LEFT JOIN' ) ) {
+			$sql['join'] = str_replace( 'INNER JOIN', 'LEFT JOIN', $sql['join'] );
+		}
+
+		/**
+		 * Filter the meta query's generated SQL.
+		 *
+		 * @since BuddyPress (2.3.0)
+		 *
+		 * @param array $args {
+		 *     An array of meta query SQL arguments.
+		 *
+		 *     @type array  $clauses           Array containing the query's JOIN and WHERE clauses.
+		 *     @type array  $queries           Array of meta queries.
+		 *     @type string $type              Type of meta.
+		 *     @type string $primary_table     Primary table.
+		 *     @type string $primary_id_column Primary column ID.
+		 *     @type object $context           The main query object.
+		 * }
+		 */
+		return apply_filters_ref_array( 'bp_xprofile_get_meta_sql', array( $sql, $this->queries, $type, $primary_table, $primary_id_column, $context ) );
+	}
+
+	/**
+	 * Generate SQL JOIN and WHERE clauses for a first-order query clause.
+	 *
+	 * "First-order" means that it's an array with a 'key' or 'value'.
+	 *
+	 * @since BuddyPress (2.3.0)
+	 * @access public
+	 *
+	 * @param array  $clause       Query clause, passed by reference.
+	 * @param array  $parent_query Parent query array.
+	 * @param string $clause_key   Optional. The array key used to name the clause in the original `$meta_query`
+	 *                             parameters. If not provided, a key will be generated automatically.
+	 * @return array {
+	 *     Array containing JOIN and WHERE SQL clauses to append to a first-order query.
+	 *
+	 *     @type string $join  SQL fragment to append to the main JOIN clause.
+	 *     @type string $where SQL fragment to append to the main WHERE clause.
+	 * }
+	 */
+	public function get_sql_for_clause( &$clause, $parent_query, $clause_key = '' ) {
+		global $wpdb;
+
+		$sql_chunks = array(
+			'where' => array(),
+			'join'  => array(),
+		);
+
+		if ( isset( $clause['compare'] ) ) {
+			$clause['compare'] = strtoupper( $clause['compare'] );
+		} else {
+			$clause['compare'] = isset( $clause['value'] ) && is_array( $clause['value'] ) ? 'IN' : '=';
+		}
+
+		if ( ! in_array( $clause['compare'], array(
+			'=', '!=', '>', '>=', '<', '<=',
+			'LIKE', 'NOT LIKE',
+			'IN', 'NOT IN',
+			'BETWEEN', 'NOT BETWEEN',
+			'EXISTS', 'NOT EXISTS',
+			'REGEXP', 'NOT REGEXP', 'RLIKE'
+		) ) ) {
+			$clause['compare'] = '=';
+		}
+
+		$meta_compare = $clause['compare'];
+
+		// First build the JOIN clause, if one is required.
+		$join = '';
+
+		// We prefer to avoid joins if possible. Look for an existing join compatible with this clause.
+		$alias = $this->find_compatible_table_alias( $clause, $parent_query );
+		if ( false === $alias ) {
+			$i = count( $this->table_aliases );
+			$alias = $i ? 'mt' . $i : $this->meta_table;
+
+			// JOIN clauses for NOT EXISTS have their own syntax.
+			if ( 'NOT EXISTS' === $meta_compare ) {
+				$join .= " LEFT JOIN $this->meta_table";
+				$join .= $i ? " AS $alias" : '';
+				$join .= $wpdb->prepare( " ON ($this->primary_table.$this->primary_id_column = $alias.$this->meta_id_column AND $alias.meta_key = %s )", $clause['key'] );
+
+			// All other JOIN clauses.
+			} else {
+				$join .= " INNER JOIN $this->meta_table";
+				$join .= $i ? " AS $alias" : '';
+				$join .= " ON ( $this->primary_table.$this->primary_id_column = $alias.$this->meta_id_column )";
+			}
+
+			$this->table_aliases[] = $alias;
+			$sql_chunks['join'][]  = $join;
+		}
+
+		// Save the alias to this clause, for future siblings to find.
+		$clause['alias'] = $alias;
+
+		// Determine the data type.
+		$_meta_type     = isset( $clause['type'] ) ? $clause['type'] : '';
+		$meta_type      = $this->get_cast_for_type( $_meta_type );
+		$clause['cast'] = $meta_type;
+
+		// Fallback for clause keys is the table alias.
+		if ( ! $clause_key ) {
+			$clause_key = $clause['alias'];
+		}
+
+		// Ensure unique clause keys, so none are overwritten.
+		$iterator = 1;
+		$clause_key_base = $clause_key;
+		while ( isset( $this->clauses[ $clause_key ] ) ) {
+			$clause_key = $clause_key_base . '-' . $iterator;
+			$iterator++;
+		}
+
+		// Store the clause in our flat array.
+		$this->clauses[ $clause_key ] =& $clause;
+
+		// Next, build the WHERE clause.
+
+		// meta_key.
+		if ( array_key_exists( 'key', $clause ) ) {
+			if ( 'NOT EXISTS' === $meta_compare ) {
+				$sql_chunks['where'][] = $alias . '.' . $this->meta_id_column . ' IS NULL';
+			} else {
+				$sql_chunks['where'][] = $wpdb->prepare( "$alias.meta_key = %s", trim( $clause['key'] ) );
+			}
+		}
+
+		// meta_value.
+		if ( array_key_exists( 'value', $clause ) ) {
+			$meta_value = $clause['value'];
+
+			if ( in_array( $meta_compare, array( 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN' ) ) ) {
+				if ( ! is_array( $meta_value ) ) {
+					$meta_value = preg_split( '/[,\s]+/', $meta_value );
+				}
+			} else {
+				$meta_value = trim( $meta_value );
+			}
+
+			switch ( $meta_compare ) {
+				case 'IN' :
+				case 'NOT IN' :
+					$meta_compare_string = '(' . substr( str_repeat( ',%s', count( $meta_value ) ), 1 ) . ')';
+					$where = $wpdb->prepare( $meta_compare_string, $meta_value );
+					break;
+
+				case 'BETWEEN' :
+				case 'NOT BETWEEN' :
+					$meta_value = array_slice( $meta_value, 0, 2 );
+					$where = $wpdb->prepare( '%s AND %s', $meta_value );
+					break;
+
+				case 'LIKE' :
+				case 'NOT LIKE' :
+					$meta_value = '%' . $wpdb->esc_like( $meta_value ) . '%';
+					$where = $wpdb->prepare( '%s', $meta_value );
+					break;
+
+				// EXISTS with a value is interpreted as '='.
+				case 'EXISTS' :
+					$meta_compare = '=';
+					$where = $wpdb->prepare( '%s', $meta_value );
+					break;
+
+				// 'value' is ignored for NOT EXISTS.
+				case 'NOT EXISTS' :
+					$where = '';
+					break;
+
+				default :
+					$where = $wpdb->prepare( '%s', $meta_value );
+					break;
+
+			}
+
+			if ( $where ) {
+				$sql_chunks['where'][] = "CAST($alias.meta_value AS {$meta_type}) {$meta_compare} {$where}";
+			}
+		}
+
+		// object_type.
+		if ( array_key_exists( 'object', $clause ) ) {
+			$object_type = $clause['object'];
+
+			if ( in_array( $meta_compare, array( 'IN', 'NOT IN' ) ) ) {
+				if ( ! is_array( $object_type ) ) {
+					$object_type = preg_split( '/[,\s]+/', $object_type );
+				}
+			} else {
+				$object_type = trim( $object_type );
+			}
+
+			switch ( $meta_compare ) {
+				case 'IN' :
+				case 'NOT IN' :
+					$meta_compare_string = '(' . substr( str_repeat( ',%s', count( $object_type ) ), 1 ) . ')';
+					$object_where        = $wpdb->prepare( $meta_compare_string, $object_type );
+					break;
+
+				case 'LIKE' :
+				case 'NOT LIKE' :
+					$object_type  = '%' . $wpdb->esc_like( $object_type ) . '%';
+					$object_where = $wpdb->prepare( '%s', $object_type );
+					break;
+
+				// EXISTS with a value is interpreted as '='.
+				case 'EXISTS' :
+					$meta_compare = '=';
+					$object_where = $wpdb->prepare( '%s', $object_type );
+					break;
+
+				// 'value' is ignored for NOT EXISTS.
+				case 'NOT EXISTS' :
+					$object_where = '';
+					break;
+
+				default :
+					$object_where = $wpdb->prepare( '%s', $object_type );
+					break;
+			}
+
+			if ( ! empty( $object_where ) ) {
+				$sql_chunks['where'][] = "{$alias}.object_type {$meta_compare} {$object_where}";
+			}
+		}
+
+		/*
+		 * Multiple WHERE clauses (for meta_key and meta_value) should
+		 * be joined in parentheses.
+		 */
+		if ( 1 < count( $sql_chunks['where'] ) ) {
+			$sql_chunks['where'] = array( '( ' . implode( ' AND ', $sql_chunks['where'] ) . ' )' );
+		}
+
+		return $sql_chunks;
+	}
+}
Index: tests/phpunit/testcases/xprofile/class-bp-xprofile-field.php
--- tests/phpunit/testcases/xprofile/class-bp-xprofile-field.php
+++ tests/phpunit/testcases/xprofile/class-bp-xprofile-field.php
@@ -54,4 +54,32 @@
 		// cleanup!
 		unset( $_POST['checkbox_option'] );
 	}
+
+	/**
+	 * @group xprofile_fields_signup
+	 */
+	public function test_get_signup_fields() {
+		$g1 = $this->factory->xprofile_group->create();
+		$f1 = $this->factory->xprofile_field->create( array(
+			'field_group_id' => $g1,
+			'type'           => 'textbox',
+		) );
+		$f2 = $this->factory->xprofile_field->create( array(
+			'field_group_id' => $g1,
+			'type'           => 'textbox',
+		) );
+		$f3 = $this->factory->xprofile_field->create( array(
+			'field_group_id' => $g1,
+			'type'           => 'textbox',
+		) );
+
+		bp_xprofile_update_meta( $f1, 'field', 'signup_position', '1' );
+		bp_xprofile_update_meta( $f2, 'field', 'signup_position', '2' );
+		bp_xprofile_update_meta( $f3, 'field', 'signup_position', '3' );
+
+		$field_ids        = array( $f1, $f2, $f3 );
+		$signup_field_ids = bp_xprofile_get_signup_field_ids();
+
+		$this->assertEquals( $field_ids, $signup_field_ids );
+	}
 }
