Page MenuHomePhabricator

"Column 'title' cannot be null" when changing CustomField
Closed, WontfixPublic

Description

I am trying to update a CustomField using a PHP script, but run into some unexpected trouble.

I had a peek at processRequest() in phabricator/src/applications/people/controller/PhabricatorPeopleProfileEditController.php and then modelled my code after buildFieldTransactionsFromRequest() from phabricator/src/infrastructure/customfield/field/PhabricatorCustomFieldList.php.

Thus my code looks like this:

function setUserField($user, $field_key, $field_value, $viewer) {
        $fields = PhabricatorCustomField::getObjectFields($user, PhabricatorCustomField::ROLE_DEFAULT)
                ->setViewer($viewer)
                ->readFieldsFromStorage($user)
                ->getFields();

        $field = idx($fields, $field_key);

        $transaction_type = $field->getApplicationTransactionType();
        $xaction = id(new PhabricatorUserTransaction())
                ->setTransactionType($transaction_type);

        if ($transaction_type == PhabricatorTransactions::TYPE_CUSTOMFIELD) {
                // For TYPE_CUSTOMFIELD transactions only, we provide the old value
                // as an input.
                $old_value = $field->getOldValueForApplicationTransactions();
                $xaction->setOldValue($old_value);
        }

        $field->getProxy()->setFieldValue($field_value);
        $new_value = $field->getNewValueForApplicationTransactions();
        $xaction->setNewValue($new_value);

        if ($transaction_type == PhabricatorTransactions::TYPE_CUSTOMFIELD) {
                // For TYPE_CUSTOMFIELD transactions, add the field key in metadata.
                $xaction->setMetadataValue('customfield:key', $field->getFieldKey());
        }

        $metadata = $field->getApplicationTransactionMetadata();
        foreach ($metadata as $key => $value) {
                $xaction->setMetadataValue($key, $value);
        }

        $xactions[] = $xaction;

        $editor = id(new PhabricatorUserProfileEditor())
                ->setActor($viewer)
                ->setContentSource(PhabricatorContentSource::newConsoleSource())
                ->setContinueOnNoEffect(true)
                ->setContinueOnMissingFields(true)
                ->applyTransactions($user, $xactions);
}

I retrieve and change the users like this:

$phab_users = id(new PhabricatorPeopleQuery())
                ->setViewer($phab_admin)
                ->withIsSystemAgent(false)
                ->withIsMailingList(false)
                ->execute();
foreach ($phab_users as $user) {
                $user->loadUserProfile();
                setUserField($user, "std:user:mycustomfield", 1234, $phab_admin);
}

This is almost the same as in the files I referenced above, but I always get an error:

# ./phabricator-apply-policy.php                                                                                                                                                                                          
[2015-06-11 21:47:06] EXCEPTION: (AphrontQueryException) #1048: Column 'title' cannot be null at [<phutil>/src/aphront/storage/connection/mysql/AphrontBaseMySQLDatabaseConnection.php:311]
arcanist(head=master, ref.master=7d15b85a1bc0), libphremoteuser(head=master, ref.master=1def4e2d7f07), phabricator(head=master, ref.master=146e2baa2032), phutil(head=master, ref.master=92882eb9404d), sprint(head=master, ref.master=ecd2a064d3c2)
  #0 AphrontBaseMySQLDatabaseConnection::throwQueryCodeException(integer, string) called at [<phutil>/src/aphront/storage/connection/mysql/AphrontBaseMySQLDatabaseConnection.php:275]
  #1 AphrontBaseMySQLDatabaseConnection::throwQueryException(mysqli) called at [<phutil>/src/aphront/storage/connection/mysql/AphrontBaseMySQLDatabaseConnection.php:181]
  #2 AphrontBaseMySQLDatabaseConnection::executeRawQuery(string) called at [<phutil>/src/xsprintf/queryfx.php:6]
  #3 queryfx(AphrontMySQLiDatabaseConnection, string, string, string, array, string)
  #4 call_user_func_array(string, array) called at [<phutil>/src/aphront/storage/connection/AphrontDatabaseConnection.php:26]
  #5 AphrontDatabaseConnection::query(string, string, string, array, string) called at [<phabricator>/src/infrastructure/storage/lisk/LiskDAO.php:1261]
  #6 LiskDAO::insertRecordIntoDatabase(string) called at [<phabricator>/src/infrastructure/storage/lisk/LiskDAO.php:1106]
  #7 LiskDAO::insert() called at [<phabricator>/src/infrastructure/storage/lisk/LiskDAO.php:1075]
  #8 LiskDAO::save() called at [<phabricator>/src/applications/people/storage/PhabricatorUser.php:269]
  #9 PhabricatorUser::save() called at [<phabricator>/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php:843]
  #10 PhabricatorApplicationTransactionEditor::applyTransactions(PhabricatorUser, array) called at [<phabricator>-apply-policy/phabricator-apply-policy.inc.php:79]
  #11 setUserField(PhabricatorUser, string, integer, PhabricatorUser) called at [<phabricator>-apply-policy/phabricator-apply-policy.inc.php:150]
  #12 apply_policies_to_users(array, array) called at [<phabricator>-apply-policy/phabricator-apply-policy.php:49]
PHP Fatal error:  Uncaught exception 'Exception' with message 'Process exited with an open transaction! The transaction will be implicitly rolled back. Calls to openTransaction() must always be paired with a call to saveTransaction() or killTransaction().' in /opt/phabricator/libphutil/src/aphront/storage/connection/AphrontDatabaseTransactionState.php:78
Stack trace:
#0 [internal function]: AphrontDatabaseTransactionState->__destruct()
#1 {main}
  thrown in /opt/phabricator/libphutil/src/aphront/storage/connection/AphrontDatabaseTransactionState.php on line 78

Fatal error: Uncaught exception 'Exception' with message 'Process exited with an open transaction! The transaction will be implicitly rolled back. Calls to openTransaction() must always be paired with a call to saveTransaction() or killTransaction().' in /opt/phabricator/libphutil/src/aphront/storage/connection/AphrontDatabaseTransactionState.php:78
Stack trace:
#0 [internal function]: AphrontDatabaseTransactionState->__destruct()
#1 {main}
  thrown in /opt/phabricator/libphutil/src/aphront/storage/connection/AphrontDatabaseTransactionState.php on line 78

Under some circumstances the user or the transaction seems to be malformed. But I don't yet see why.

Event Timeline

devurandom raised the priority of this task from to Needs Triage.
devurandom updated the task description. (Show Details)
devurandom added a subscriber: devurandom.

The users I tried this with all do not have a user profile set in the database. Thus I assume Phabricator tries to create blank profiles, when it applies the changes to the CustomFields, which then fails to be inserted, since these do not comply with the database schema?

I inserted print(idx($fields, "user:title")); into setUserField(), though, and that throws an exception which tells me that this field is of type PhabricatorUserTitleField and not NULL.

epriestley claimed this task.
epriestley added a subscriber: epriestley.

Sorry, we don't offer support for writing custom extensions or applications. See T5447.

It seems indeed that the profile created by $user->loadUserProfile() is broken or incomplete.

I solved this using the following code:

function initUserProfile($user, $viewer) {
        print("Initialising user: " . $user->getUsername() . "\n");

        $profile = $user->loadUserProfile();

        print("  Title: " . $profile->getTitle() . "\n");
        if ($profile->getTitle() === null) {
                $profile->setTitle("");
                print("  New title: " . $profile->getTitle() . "\n");
        }

        print("  Blurb: " . $profile->getBlurb() . "\n");
        if ($profile->getBlurb() === null) {
                $profile->setBlurb("");
                print("  New blurb: " . $profile->getBlurb() . "\n");
        }

        $profile->save();
}

@epriestley: Preferrable over initialising the user profile manually would of course be, if it would be initialised with empty strings upon creation.

As I understand LiskDAO, it is not necessary to build an own transaction around the init code, as save() does that already.

@epriestley: I understand your concerns about supporting custom scripts, but what do you think about creating initialised user profiles? Currently it seems that loadUserProfile() just dumps something with a PHID on you (when the profile did not yet exist), which is not actually a valid object. The fix seems trivial.

Please see the code in my previous comment, if it is not clear what I mean.

We aren't interested in pursuing changes with no upstream impact.