SeaQL/sea-query

SQLite: Booleans become integer columns

reivilibre opened this issue · 6 comments

Description

When using SQLite, boolean data types are converted to integer.

Whilst this is somewhat fair enough (SQLite doesn't have a native boolean type; it expects you to use an integer with 0 and 1), it means that sea-orm-cli generate entity gives you a i32 field rather than a bool field, which is ... sad.

This conflicts a little bit with the 'schema-first' goal of this project, as we lose information by encoding it into the schema and then decoding it again.

See

ColumnType::Boolean => "integer".into(),
.

Steps to Reproduce

  1. Create a schema migration including a boolean type, e.g.:
        manager
            .create_table(
                Table::create()
                    .table(TimelineSegment::Table)
                    .if_not_exists()
                    .col(
                        ColumnDef::new(TimelineSegment::SegmentId)
                            .integer()
                            .not_null()
                            .auto_increment()
                            .primary_key(),
                    )
                    .col(
                        ColumnDef::new(TimelineSegment::PrevGappy)
                            .boolean()
                            .not_null(),
                    )
                    .to_owned(),
            )
            .await?;
  1. Apply the migration
  2. Generate entities using sea-orm-cli generate entity --database-url sqlite://$(realpath ../../../devdb.sqlite3)

Expected Behavior

I should have obtained this entity:

#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
#[sea_orm(table_name = "timeline_segment")]
pub struct Model {
    #[sea_orm(primary_key)]
    pub segment_id: i32,
    pub prev_gappy: bool,
}

Actual Behavior

I obtained this entity:

#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
#[sea_orm(table_name = "timeline_segment")]
pub struct Model {
    #[sea_orm(primary_key)]
    pub segment_id: i32,
    pub prev_gappy: i32,  // <---- !!!
}

SQLite's schema shows the reason why quite clearly:

sqlite> .schema
CREATE TABLE IF NOT EXISTS "seaql_migrations" ( "version" text NOT NULL PRIMARY KEY, "applied_at" integer NOT NULL );
CREATE TABLE IF NOT EXISTS "timeline_segment" ( "segment_id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "prev_gappy" integer NOT NULL );

Reproduces How Often

Always

Versions

│   └── sea-orm v0.8.0
│       ├── sea-orm-macros v0.8.0 (proc-macro)
│       ├── sea-query v0.24.6
│       │   ├── sea-query-derive v0.2.0 (proc-macro)
│       │   ├── sea-query-driver v0.1.1 (proc-macro)
│       ├── sea-strum v0.23.0
│       │   └── sea-strum_macros v0.23.0 (proc-macro)
├── sea-orm v0.8.0 (*)
├── sea-query v0.25.2
│   └── sea-query-derive v0.2.0 (proc-macro) (*)

Using SQLite on Ubuntu.

Additional Information

If we think maintaining the integer affinity is important, we could be cheeky and call the type integerboolean or something like that, which the entity generator could recognise to mean boolean.

The algorithm at https://www.sqlite.org/datatype3.html#determination_of_column_affinity suggests that will be absolutely fine as long as the name contains 'int'.

Interesting idea. Definitely worth doing!

Hi @tyt2y3 and @billy1624
I would like to work on this issue. I also personally use SQLite and ran into this problem where I was manually converting ints to boolean, and vice-versa, in my project. So I'd like to see this feature supported.

Note, that this issue is marked "good-first-issue" and I have already worked on a "good-first-issue" last week SeaQL/sea-orm#869

@anshulxyz hello! Thanks you for interest in this issue! If I can help you let me know.

Hi, I was looking at Django's code for reference and I found this.

https://github.com/django/django/blob/d38324edc840049d42c3454b9487ac370aab5ee9/django/db/backends/sqlite3/base.py#L52-L62

class DatabaseWrapper(BaseDatabaseWrapper):
    vendor = "sqlite"
    display_name = "SQLite"
    # SQLite doesn't actually support most of these types, but it "does the right
    # thing" given more verbose field definitions, so leave them as is so that
    # schema inspection is more useful.
    data_types = {
        "AutoField": "integer",
        "BigAutoField": "integer",
        "BinaryField": "BLOB",
        "BooleanField": "bool",

So I put it to test in raw SQLite

sqlite> CREATE TABLE "test" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "is_done" bool NOT NULL );
sqlite> .tables
test
sqlite> .schema test
CREATE TABLE IF NOT EXISTS "test" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "is_done" bool NOT NULL );
sqlite> INSERT INTO test ("is_done") VALUES (1);
sqlite> INSERT INTO test ("is_done") VALUES (0);
sqlite> INSERT INTO test ("is_done") VALUES (2);
sqlite> SELECT * FROM test;
1|1
2|0
3|2

So this works!

I noticed that SQLite already does the conversion for us.

sqlite> CREATE TABLE "test" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "is_done" bool NOT NULL );
sqlite> .schema test
CREATE TABLE IF NOT EXISTS "test" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "is_done" bool NOT NULL );
sqlite> INSERT INTO test ("is_done") VALUES (TRUE);
sqlite> INSERT INTO test ("is_done") VALUES (FALSE);
sqlite> SELECT * FROM test;
1|1
2|0

Notice how it already converted TRUE to 1 and FALSE to 0.

Update:

SELECT statements work as well.

sqlite> SELECT * FROM test WHERE is_done = TRUE;
1|1

I have added a test case for it. It passes. anshulxyz@688e41c