Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions docs/tutorial/str-fields-and-column-length.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# String fields and column length

Some databases have a limit on the length of string columns (e.g., `VARCHAR(255)` in *MySQL*) and fail with an error if you try to create a string column without specifying a length.

**SQLModel** handles this automatically depending on the database dialect you are using. 😎

For databases that require a length for string columns, **SQLModel** will automatically set a default length (e.g., `255` for *MySQL*) if you do not specify one.

{* ./docs_src/tutorial/str_fields_and_column_length/tutorial001_py310.py ln[4:6] hl[6] *}

If you run this code with *MySQL*, **SQLModel** will create the `name` column as `VARCHAR(255)`:

```sql
CREATE TABLE hero (
id INTEGER NOT NULL AUTO_INCREMENT,
name VARCHAR(255) NOT NULL,
PRIMARY KEY (id)
)
```

But you can always override this by specifying a custom length if needed:

{* ./docs_src/tutorial/str_fields_and_column_length/tutorial002_py310.py ln[4:6] hl[6] *}

```sql
CREATE TABLE hero (
id INTEGER NOT NULL AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
PRIMARY KEY (id)
)
```
This works thanks to `AutoString` type that **SQLModel** uses for all string fields by default.

But if you specify the database type of column explicitly, **SQLModel** will not be able to set the length automatically, and you will need to specify it manually:

{* ./docs_src/tutorial/str_fields_and_column_length/tutorial003_py310.py ln[1:6] hl[1,6] *}

The code example above will fail on databases that require a length for string columns:

```console
sqlalchemy.exc.CompileError: (in table 'hero', column 'name'): VARCHAR requires a length on dialect mysql
```

To fix it, you need to specify the length explicitly as follows:

{* ./docs_src/tutorial/str_fields_and_column_length/tutorial004_py310.py ln[1:6] hl[1,6] *}

This will give:

```sql
CREATE TABLE hero (
id INTEGER NOT NULL AUTO_INCREMENT,
name VARCHAR(255) NOT NULL,
PRIMARY KEY (id)
)
```
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from sqlmodel import Field, SQLModel, create_engine


class Hero(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
name: str


database_url = "mysql://user:password@localhost/dbname"

engine = create_engine(database_url, echo=True)


def create_db_and_tables():
SQLModel.metadata.create_all(engine)


if __name__ == "__main__":
create_db_and_tables()
21 changes: 21 additions & 0 deletions docs_src/tutorial/str_fields_and_column_length/tutorial001_py39.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from typing import Optional

from sqlmodel import Field, SQLModel, create_engine


class Hero(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
name: str


database_url = "mysql://user:password@localhost/dbname"

engine = create_engine(database_url, echo=True)


def create_db_and_tables():
SQLModel.metadata.create_all(engine)


if __name__ == "__main__":
create_db_and_tables()
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from sqlmodel import Field, SQLModel, create_engine


class Hero(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
name: str = Field(max_length=100)


database_url = "mysql://user:password@localhost/dbname"

engine = create_engine(database_url, echo=True)


def create_db_and_tables():
SQLModel.metadata.create_all(engine)


if __name__ == "__main__":
create_db_and_tables()
21 changes: 21 additions & 0 deletions docs_src/tutorial/str_fields_and_column_length/tutorial002_py39.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from typing import Optional

from sqlmodel import Field, SQLModel, create_engine


class Hero(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
name: str = Field(max_length=100)


database_url = "mysql://user:password@localhost/dbname"

engine = create_engine(database_url, echo=True)


def create_db_and_tables():
SQLModel.metadata.create_all(engine)


if __name__ == "__main__":
create_db_and_tables()
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from sqlmodel import Field, SQLModel, String, create_engine


class Hero(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
name: str = Field(sa_type=String)


database_url = "mysql://user:password@localhost/dbname"

engine = create_engine(database_url, echo=True)


def create_db_and_tables():
SQLModel.metadata.create_all(engine)


if __name__ == "__main__":
create_db_and_tables()
21 changes: 21 additions & 0 deletions docs_src/tutorial/str_fields_and_column_length/tutorial003_py39.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from typing import Optional

from sqlmodel import Field, SQLModel, String, create_engine


class Hero(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
name: str = Field(sa_type=String)


database_url = "mysql://user:password@localhost/dbname"

engine = create_engine(database_url, echo=True)


def create_db_and_tables():
SQLModel.metadata.create_all(engine)


if __name__ == "__main__":
create_db_and_tables()
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from sqlmodel import Field, SQLModel, String, create_engine


class Hero(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
name: str = Field(sa_type=String(length=255))


database_url = "mysql://user:password@localhost/dbname"

engine = create_engine(database_url, echo=True)


def create_db_and_tables():
SQLModel.metadata.create_all(engine)


if __name__ == "__main__":
create_db_and_tables()
21 changes: 21 additions & 0 deletions docs_src/tutorial/str_fields_and_column_length/tutorial004_py39.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from typing import Optional

from sqlmodel import Field, SQLModel, String, create_engine


class Hero(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
name: str = Field(sa_type=String(length=255))


database_url = "mysql://user:password@localhost/dbname"

engine = create_engine(database_url, echo=True)


def create_db_and_tables():
SQLModel.metadata.create_all(engine)


if __name__ == "__main__":
create_db_and_tables()
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ nav:
- tutorial/create-db-and-table.md
- tutorial/insert.md
- tutorial/automatic-id-none-refresh.md
- tutorial/str-fields-and-column-length.md
- tutorial/select.md
- tutorial/where.md
- tutorial/indexes.md
Expand Down
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import importlib
import runpy
import sys
from collections.abc import Generator
from types import ModuleType
from unittest.mock import patch

import pytest
from sqlalchemy import create_mock_engine
from sqlalchemy.sql.type_api import TypeEngine
from sqlmodel import create_engine

from ...conftest import needs_py310


def mysql_dump(sql: TypeEngine, *args, **kwargs):
dialect = sql.compile(dialect=mysql_engine.dialect)
sql_str = str(dialect).rstrip()
if sql_str:
print(sql_str + ";")


mysql_engine = create_mock_engine("mysql://", mysql_dump)


@pytest.fixture(
name="mod",
params=[
pytest.param("tutorial001_py39"),
pytest.param("tutorial001_py310", marks=needs_py310),
],
)
def get_module(request: pytest.FixtureRequest) -> Generator[ModuleType, None, None]:
with patch("sqlmodel.create_engine"): # To avoid "No module named 'MySQLdb'" error
mod = importlib.import_module(
f"docs_src.tutorial.str_fields_and_column_length.{request.param}"
)
yield mod


def test_sqlite_ddl_sql(mod: ModuleType, caplog: pytest.LogCaptureFixture):
mod.sqlite_url = "sqlite://"
mod.engine = create_engine(mod.sqlite_url, echo=True)
mod.create_db_and_tables()
assert "CREATE TABLE hero (" in caplog.text
assert "name VARCHAR NOT NULL" in caplog.text


def test_mysql_ddl_sql(mod: ModuleType, capsys: pytest.CaptureFixture[str]):
importlib.reload(mod)

mod.SQLModel.metadata.create_all(bind=mysql_engine, checkfirst=False)
captured = capsys.readouterr()
assert "CREATE TABLE hero (" in captured.out
assert "name VARCHAR(255) NOT NULL" in captured.out


# For coverage
def test_run_main(mod: ModuleType):
# Remove module to avoid double-import warning
sys.modules.pop(mod.__name__, None)

runpy.run_module(mod.__name__, run_name="__main__")
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import importlib
import runpy
import sys
from collections.abc import Generator
from types import ModuleType
from unittest.mock import patch

import pytest
from sqlalchemy import create_mock_engine
from sqlalchemy.sql.type_api import TypeEngine
from sqlmodel import create_engine

from ...conftest import needs_py310


def mysql_dump(sql: TypeEngine, *args, **kwargs):
dialect = sql.compile(dialect=mysql_engine.dialect)
sql_str = str(dialect).rstrip()
if sql_str:
print(sql_str + ";")


mysql_engine = create_mock_engine("mysql://", mysql_dump)


@pytest.fixture(
name="mod",
params=[
pytest.param("tutorial002_py39"),
pytest.param("tutorial002_py310", marks=needs_py310),
],
)
def get_module(request: pytest.FixtureRequest) -> Generator[ModuleType, None, None]:
with patch("sqlmodel.create_engine"): # To avoid "No module named 'MySQLdb'" error
mod = importlib.import_module(
f"docs_src.tutorial.str_fields_and_column_length.{request.param}"
)
yield mod


def test_sqlite_ddl_sql(mod: ModuleType, caplog: pytest.LogCaptureFixture):
mod.sqlite_url = "sqlite://"
mod.engine = create_engine(mod.sqlite_url, echo=True)
mod.create_db_and_tables()
assert "CREATE TABLE hero (" in caplog.text
assert "name VARCHAR(100) NOT NULL" in caplog.text


def test_mysql_ddl_sql(mod: ModuleType, capsys: pytest.CaptureFixture[str]):
importlib.reload(mod)

mod.SQLModel.metadata.create_all(bind=mysql_engine, checkfirst=False)
captured = capsys.readouterr()
assert "CREATE TABLE hero (" in captured.out
assert "name VARCHAR(100) NOT NULL" in captured.out


# For coverage
def test_run_main(mod: ModuleType):
# Remove module to avoid double-import warning
sys.modules.pop(mod.__name__, None)

runpy.run_module(mod.__name__, run_name="__main__")
Loading