Skip to content

null vs empty string #353

@trondhindenes

Description

@trondhindenes

given a model:

class Customer(Table):
    email: str = Varchar()

this migration is auto-generated:

    manager.add_column(
        table_class_name="Customer",
        tablename="customer",
        column_name="email",
        db_column_name="email",
        column_class_name="Varchar",
        column_class=Varchar,
        params={
            "length": 255,
            "default": "",
            "null": False,
            "primary_key": False,
            "unique": False,
            "index": False,
            "index_method": IndexMethod.btree,
            "choices": None,
            "db_column_name": None,
            "secret": False,
        },
    )

One would think that this would disallow creating a record with an empty email field, but it is allowed:

    customer = Customer()
    customer.save().run_sync()

the sql statement generated from the migration is:

execute __asyncpg_stmt_6__: CREATE TABLE customer ("id" SERIAL PRIMARY KEY NOT NULL, "email" VARCHAR(255) NOT NULL DEFAULT '')

the returned object when queriyng is:

{'id': 1, 'email': ''}

As far as I can see, the bug here is that piccolo sets an empty string as the default for the column, which is the default for posgres. IMHO it would be "more correct" if piccolo disallowed creating objects where non-nullable columns are not specified (it would also be closer to how pydantic works)

Another (and maybe bigger) issue is that setting a column to nullable has some consequences:
the generated sql:

2021-11-15 21:09:24.996 UTC [89] LOG:  execute __asyncpg_stmt_6__: CREATE TABLE customer ("id" SERIAL PRIMARY KEY NOT NULL, "email" VARCHAR(255) DEFAULT '')

...and the code:

    customer = Customer()
    customer.save().run_sync()

    this_cust = Customer.objects().first().run_sync()
    assert this_cust.email is None

this fails, since the email field is empty string instead of None.

even tho an empty string is "non-truthy" in python, treating them as true None would imho be much cleaner.

suggestion

In order not to break backwards compatability maybe there could be a flag in the engine config for configuring "empty string is None", and if true, replace empty strings for nullable fields with a proper None. Another flag could be "fail fast non-nullable-fields" where the engine intercepts/raises a validation error if attempting to write an object with an unset non-nullable field to the database.

So, summing up:

  • piccolo allows "non-nullable" collumns to be empty strings (which is the same as is used for "None" values, see item 3)
  • piccolo allows "non-nullable" colums to be left unset on object creation (I would expect it to throw prior to writing to the db)
  • piccolo returns empty strings instead of None for nullable fields

All of these have confused me somewhat during development with piccolo the last week. I may have overlooked something in the docs describing piccolo's "null" handling but I couldn't find anything.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions