Error when inserting `false` into a boolean column with underscore in column name

Inserting false into a boolean column yields an error ONLY when the column name includes an underscore. Passing true or null works. Also, if the column name does not include an underscore, the error does not occur.

I’m using the docker dev repo.

Error message

Using mysql

An exception occurred while executing a query: SQLSTATE[22007]: Invalid datetime format: 1366 Incorrect integer value: '' for column `nextcloud`.`oc_test_foos`.`bar_baz` at row 1

Using pgsql

An exception occurred while executing a query: SQLSTATE[22P02]: Invalid text representation: 7 ERROR:  invalid input syntax for type boolean: \"\"\nCONTEXT:  unnamed portal parameter $1 = ''

Example code

Migration

<?php

declare(strict_types=1);

namespace OCA\Test\Migration;

use Closure;
use OCP\DB\ISchemaWrapper;
use OCP\DB\Types;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;

class Version010000Date20240926220000 extends SimpleMigrationStep
{
    public function changeSchema(IOutput $output, Closure $schemaClosure, array $options)
    {
        /** @var ISchemaWrapper $schema */
        $schema = $schemaClosure();

        if (!$schema->hasTable('test_foos')) {
            $table = $schema->createTable('test_foos');
            $table->addColumn('id', Types::BIGINT, ['autoincrement' => true]);
            $table->addColumn('bar_baz', Types::BOOLEAN, ['notnull' => false, 'default' => false]);
            $table->setPrimaryKey(['id']);
        }

        return $schema;
    }
}

Entity

<?php

declare(strict_types=1);

namespace OCA\Test\Db;

use JsonSerializable;
use OCP\AppFramework\Db\Entity;

/**
 * @method bool getBarBaz()
 * @method void setBarBaz(bool $barBaz)
 */
class Foo extends Entity implements JsonSerializable
{
    /** @var bool */
    protected $barBaz;

    public function __construct()
    {
        $this->addType('bar_baz', 'boolean');
    }

    public function jsonSerialize(): mixed
    {
        return [
            'id' => $this->id,
            'bar_baz' => $this->barBaz,
        ];
    }
}

Mapper

<?php

declare(strict_types=1);

namespace OCA\Test\Db;

use OCP\AppFramework\Db\QBMapper;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;

class FooMapper extends QBMapper
{
    public function __construct(IDBConnection $db)
    {
        parent::__construct($db, 'test_foos', Foo::class);
    }

    public function create(bool $barBaz): Foo
    {
        $foo = new Foo();
        $foo->setBarBaz($barBaz);
        return $this->insert($foo);
    }
}

Controller

<?php

declare(strict_types=1);

namespace OCA\Test\Controller;

use OCA\Test\Db\FooMapper;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\FrontpageRoute;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
use OCP\AppFramework\Http\DataResponse;
use OCP\IRequest;
use Throwable;

class FooController extends Controller
{
    public function __construct(
        string $appName,
        IRequest $request,
        private FooMapper $fooMapper,
    ) {
        parent::__construct($appName, $request);
    }

    #[NoAdminRequired]
    #[NoCSRFRequired]
    #[FrontpageRoute(verb: 'POST', url: '/foos')]
    public function create(bool $bar_baz): DataResponse
    {
        try {
            return new DataResponse($this->fooMapper->create($bar_baz));
        } catch (Throwable $th) {
            return new DataResponse(['error' => $th->getMessage()], Http::STATUS_BAD_REQUEST);
        }
    }
}

Hello.

I just see one thing from the static analysis: You write

$this->addType('bar_baz', 'boolean');

As far as I see the source code in the server, you need to provide the name of the field in the class here, that would be

$this->addType('barBaz', 'boolean');

Unfortunately, I cannot help here more without actually tesing the code and debugging the Doctrine classes what the actual SQL command is and what caused this. I have to use my dev machine there.

So, please check if that small change made it (I fear this is not everything) and I will try to dig into this.

Christian

2 Likes

Hmm, this did indeed solve the problem. The reason I used the snake cased version in the call to the addType method is because that’s what the tutorial tells us to.

<?php

declare(strict_types=1);

namespace OCA\NoteBook\Db;

use OCP\AppFramework\Db\Entity;

/**
 * @method string|null getUserId()
 * @method void setUserId(?string $userId)
 * @method string getName()
 * @method void setName(string $name)
 * @method string getContent()
 * @method void setContent(string $content)
 * @method int getLastModified()
 * @method void setLastModified(int $lastModified)
 */
class Note extends Entity implements \JsonSerializable {

	/** @var string */
	protected $userId;
	/** @var string */
	protected $name;
	/** @var string */
	protected $content;
	/** @var int */
	protected $lastModified;

	public function __construct() {
		$this->addType('user_id', 'string');
		$this->addType('name', 'string');
		$this->addType('content', 'string');
		$this->addType('last_modified', 'integer');
	}

	#[\ReturnTypeWillChange]
	public function jsonSerialize() {
		return [
			'id' => $this->id,
			'user_id' => $this->userId,
			'name' => $this->name,
			'content' => $this->content,
			'last_modified' => (int) $this->lastModified,
		];
	}
}

But the question still stands, why this problem does only occur with boolean columns.

Unfortunately, I cannot give you a final answer. I just see that the abstract mapper class falls back to string if the column was not found. You would have to dig into doctrine or even the database implementation to find out why this is failing the way it does.

@Daphne who could verify the assumption that the attribute name and not the table name is needed here and probably update the documentation? Or should I send you again a changes file?

1 Like

I had a quick look at some other (official?) NC apps, and they use the entity class property name (camel case) instead of the table column name (snake case), so I would conclude that you are right. The tutorial needs to be updated though.

addType needs the property of the class, not the field in the column.

Please excuse the typo in our tutorial.

I will forward it.

2 Likes

Entities: Database access — Nextcloud latest Developer Manual latest documentation

The dev manual is not really any help here.

Entities

Entities are data objects that carry all the table’s information for one row. Every Entity has an id field by default that is set to the integer type. Table rows are mapped from lower case and underscore separated names to lowerCamelCase attributes:

  • Table column name: phone_number
  • Property name: phoneNumber
<?php

namespace OCA\MyApp\Db;

use OCP\AppFramework\Db\Entity;

class Author extends Entity {

    protected $stars;
    protected $name;
    protected $phoneNumber;

    public function __construct() {
        // add types in constructor
        $this->addType('stars', 'integer');
    }
}

There are multiple issues here:

  1. I am pretty sure it should say “Table columns are mapped…” instead of “Table rows are mapped…”
  2. People (me included) might think the word “attributes” only refers to the actual declaration of the class properties, e.g. protected $phoneNumber; etc.
  3. The code example only shows the first invokation of the addType method: $this->addType('stars', 'integer'); where it would’ve been better to include $this->addType('phoneNumber', 'string'); to make things clear

Of you want you can file a PR otherwise I might do so the next day(s). Just say/post what you intent to do.

I don’t understand…

Sorry, the mobile phone “auto mis-corrected” my sentence :see_no_evil:. I fixed the original post.

@edward updated the tutorial.

2 Likes