From 9ba902b33ca529971179e567ce2c4f4479360d64 Mon Sep 17 00:00:00 2001 From: Yurii Motov Date: Tue, 27 Jan 2026 14:19:33 +0100 Subject: [PATCH 1/3] Add "String fields and column length" sections to docs --- docs/tutorial/str-fields-and-column-length.md | 56 +++++++++++++++++++ .../str_fields_and_column_length/__init__.py | 0 .../tutorial001_py310.py | 19 +++++++ .../tutorial001_py39.py | 21 +++++++ .../tutorial002_py310.py | 19 +++++++ .../tutorial002_py39.py | 21 +++++++ .../tutorial003_py310.py | 19 +++++++ .../tutorial003_py39.py | 21 +++++++ .../tutorial004_py310.py | 19 +++++++ .../tutorial004_py39.py | 21 +++++++ mkdocs.yml | 1 + 11 files changed, 217 insertions(+) create mode 100644 docs/tutorial/str-fields-and-column-length.md create mode 100644 docs_src/tutorial/str_fields_and_column_length/__init__.py create mode 100644 docs_src/tutorial/str_fields_and_column_length/tutorial001_py310.py create mode 100644 docs_src/tutorial/str_fields_and_column_length/tutorial001_py39.py create mode 100644 docs_src/tutorial/str_fields_and_column_length/tutorial002_py310.py create mode 100644 docs_src/tutorial/str_fields_and_column_length/tutorial002_py39.py create mode 100644 docs_src/tutorial/str_fields_and_column_length/tutorial003_py310.py create mode 100644 docs_src/tutorial/str_fields_and_column_length/tutorial003_py39.py create mode 100644 docs_src/tutorial/str_fields_and_column_length/tutorial004_py310.py create mode 100644 docs_src/tutorial/str_fields_and_column_length/tutorial004_py39.py diff --git a/docs/tutorial/str-fields-and-column-length.md b/docs/tutorial/str-fields-and-column-length.md new file mode 100644 index 0000000000..8d08ab4db5 --- /dev/null +++ b/docs/tutorial/str-fields-and-column-length.md @@ -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 specify 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) +) +``` diff --git a/docs_src/tutorial/str_fields_and_column_length/__init__.py b/docs_src/tutorial/str_fields_and_column_length/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/tutorial/str_fields_and_column_length/tutorial001_py310.py b/docs_src/tutorial/str_fields_and_column_length/tutorial001_py310.py new file mode 100644 index 0000000000..a73201e2aa --- /dev/null +++ b/docs_src/tutorial/str_fields_and_column_length/tutorial001_py310.py @@ -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() diff --git a/docs_src/tutorial/str_fields_and_column_length/tutorial001_py39.py b/docs_src/tutorial/str_fields_and_column_length/tutorial001_py39.py new file mode 100644 index 0000000000..ff33483588 --- /dev/null +++ b/docs_src/tutorial/str_fields_and_column_length/tutorial001_py39.py @@ -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() diff --git a/docs_src/tutorial/str_fields_and_column_length/tutorial002_py310.py b/docs_src/tutorial/str_fields_and_column_length/tutorial002_py310.py new file mode 100644 index 0000000000..b8a1119d46 --- /dev/null +++ b/docs_src/tutorial/str_fields_and_column_length/tutorial002_py310.py @@ -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() diff --git a/docs_src/tutorial/str_fields_and_column_length/tutorial002_py39.py b/docs_src/tutorial/str_fields_and_column_length/tutorial002_py39.py new file mode 100644 index 0000000000..32e7052b18 --- /dev/null +++ b/docs_src/tutorial/str_fields_and_column_length/tutorial002_py39.py @@ -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() diff --git a/docs_src/tutorial/str_fields_and_column_length/tutorial003_py310.py b/docs_src/tutorial/str_fields_and_column_length/tutorial003_py310.py new file mode 100644 index 0000000000..51e0c1bb4a --- /dev/null +++ b/docs_src/tutorial/str_fields_and_column_length/tutorial003_py310.py @@ -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() diff --git a/docs_src/tutorial/str_fields_and_column_length/tutorial003_py39.py b/docs_src/tutorial/str_fields_and_column_length/tutorial003_py39.py new file mode 100644 index 0000000000..4ca5ff0f0d --- /dev/null +++ b/docs_src/tutorial/str_fields_and_column_length/tutorial003_py39.py @@ -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() diff --git a/docs_src/tutorial/str_fields_and_column_length/tutorial004_py310.py b/docs_src/tutorial/str_fields_and_column_length/tutorial004_py310.py new file mode 100644 index 0000000000..fb67058ebf --- /dev/null +++ b/docs_src/tutorial/str_fields_and_column_length/tutorial004_py310.py @@ -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() diff --git a/docs_src/tutorial/str_fields_and_column_length/tutorial004_py39.py b/docs_src/tutorial/str_fields_and_column_length/tutorial004_py39.py new file mode 100644 index 0000000000..c1ef56ab7e --- /dev/null +++ b/docs_src/tutorial/str_fields_and_column_length/tutorial004_py39.py @@ -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() diff --git a/mkdocs.yml b/mkdocs.yml index b89516e024..42d9d79b0c 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -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 From 9c5c51b230acde7cee73cf0635bf190ab57a6ee3 Mon Sep 17 00:00:00 2001 From: Yurii Motov Date: Tue, 27 Jan 2026 16:49:52 +0100 Subject: [PATCH 2/3] Add tests --- .../__init__.py | 0 .../test_tutorial001.py | 63 ++++++++++++++++++ .../test_tutorial002.py | 63 ++++++++++++++++++ .../test_tutorial003.py | 64 +++++++++++++++++++ .../test_tutorial004.py | 63 ++++++++++++++++++ 5 files changed, 253 insertions(+) create mode 100644 tests/test_tutorial/test_str_fields_and_column_length/__init__.py create mode 100644 tests/test_tutorial/test_str_fields_and_column_length/test_tutorial001.py create mode 100644 tests/test_tutorial/test_str_fields_and_column_length/test_tutorial002.py create mode 100644 tests/test_tutorial/test_str_fields_and_column_length/test_tutorial003.py create mode 100644 tests/test_tutorial/test_str_fields_and_column_length/test_tutorial004.py diff --git a/tests/test_tutorial/test_str_fields_and_column_length/__init__.py b/tests/test_tutorial/test_str_fields_and_column_length/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/test_tutorial/test_str_fields_and_column_length/test_tutorial001.py b/tests/test_tutorial/test_str_fields_and_column_length/test_tutorial001.py new file mode 100644 index 0000000000..755457eb85 --- /dev/null +++ b/tests/test_tutorial/test_str_fields_and_column_length/test_tutorial001.py @@ -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__") diff --git a/tests/test_tutorial/test_str_fields_and_column_length/test_tutorial002.py b/tests/test_tutorial/test_str_fields_and_column_length/test_tutorial002.py new file mode 100644 index 0000000000..11be8afb7e --- /dev/null +++ b/tests/test_tutorial/test_str_fields_and_column_length/test_tutorial002.py @@ -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__") diff --git a/tests/test_tutorial/test_str_fields_and_column_length/test_tutorial003.py b/tests/test_tutorial/test_str_fields_and_column_length/test_tutorial003.py new file mode 100644 index 0000000000..6302771917 --- /dev/null +++ b/tests/test_tutorial/test_str_fields_and_column_length/test_tutorial003.py @@ -0,0 +1,64 @@ +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.exc import CompileError +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("tutorial003_py39"), + pytest.param("tutorial003_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): + importlib.reload(mod) + + with pytest.raises(CompileError) as exc_info: + mod.SQLModel.metadata.create_all(bind=mysql_engine, checkfirst=False) + + assert "VARCHAR requires a length on dialect mysql" in str(exc_info.value) + + +# 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__") diff --git a/tests/test_tutorial/test_str_fields_and_column_length/test_tutorial004.py b/tests/test_tutorial/test_str_fields_and_column_length/test_tutorial004.py new file mode 100644 index 0000000000..f6367499c0 --- /dev/null +++ b/tests/test_tutorial/test_str_fields_and_column_length/test_tutorial004.py @@ -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("tutorial004_py39"), + pytest.param("tutorial004_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(255) 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__") From 438f7d995d8f1ffdb99765ed6c06766e66e4f05f Mon Sep 17 00:00:00 2001 From: Yurii Motov Date: Tue, 27 Jan 2026 17:42:24 +0100 Subject: [PATCH 3/3] Minor fix --- docs/tutorial/str-fields-and-column-length.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/str-fields-and-column-length.md b/docs/tutorial/str-fields-and-column-length.md index 8d08ab4db5..35d03cf1c2 100644 --- a/docs/tutorial/str-fields-and-column-length.md +++ b/docs/tutorial/str-fields-and-column-length.md @@ -18,7 +18,7 @@ CREATE TABLE hero ( ) ``` -But you can always specify a custom length if needed: +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] *}