mirror of
https://github.com/simonw/datasette.git
synced 2026-06-23 01:04:49 +02:00
not_null, default and default_exr support for create table API columns
This commit is contained in:
parent
a2e75967ce
commit
87354cf94e
3 changed files with 119 additions and 21 deletions
|
|
@ -319,13 +319,28 @@ class _StrictPydanticModel(BaseModel):
|
|||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
class CreateTableColumn(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
class _DefaultArgsMixin(_StrictPydanticModel):
|
||||
default: Any | None = None
|
||||
default_expr: DefaultExpr | None = None
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_default_fields(self):
|
||||
has_default = "default" in self.model_fields_set
|
||||
has_default_expr = "default_expr" in self.model_fields_set
|
||||
if has_default and has_default_expr:
|
||||
raise ValueError("default and default_expr cannot both be provided")
|
||||
if has_default_expr and self.default_expr is None:
|
||||
raise ValueError("default_expr cannot be null")
|
||||
return self
|
||||
|
||||
|
||||
class CreateTableColumn(_DefaultArgsMixin):
|
||||
|
||||
name: Any = None
|
||||
type: Any = "text"
|
||||
fk_table: str | None = None
|
||||
fk_column: str | None = None
|
||||
not_null: bool = False
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_column(self):
|
||||
|
|
@ -450,21 +465,6 @@ class CreateTableRequest(_StrictPydanticModel):
|
|||
return foreign_keys or None
|
||||
|
||||
|
||||
class _DefaultArgsMixin(_StrictPydanticModel):
|
||||
default: Any | None = None
|
||||
default_expr: DefaultExpr | None = None
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_default_fields(self):
|
||||
has_default = "default" in self.model_fields_set
|
||||
has_default_expr = "default_expr" in self.model_fields_set
|
||||
if has_default and has_default_expr:
|
||||
raise ValueError("default and default_expr cannot both be provided")
|
||||
if has_default_expr and self.default_expr is None:
|
||||
raise ValueError("default_expr cannot be null")
|
||||
return self
|
||||
|
||||
|
||||
class AddColumnArgs(_DefaultArgsMixin):
|
||||
name: str
|
||||
type: SqliteApiType = "text"
|
||||
|
|
@ -753,16 +753,33 @@ class TableCreateView(BaseView):
|
|||
)
|
||||
|
||||
def create_table(conn):
|
||||
table = sqlite_utils.Database(conn)[table_name]
|
||||
db_for_write = sqlite_utils.Database(conn)
|
||||
table = db_for_write[table_name]
|
||||
if rows:
|
||||
table.insert_all(
|
||||
rows, pk=pks or pk, ignore=ignore, replace=replace, alter=alter
|
||||
)
|
||||
else:
|
||||
not_null = [column.name for column in columns if column.not_null]
|
||||
defaults = {}
|
||||
for column in columns:
|
||||
if "default_expr" in column.model_fields_set:
|
||||
defaults[column.name] = _default_expression_sql(
|
||||
column.default_expr
|
||||
)
|
||||
elif (
|
||||
"default" in column.model_fields_set
|
||||
and column.default is not None
|
||||
):
|
||||
defaults[column.name] = _literal_default(
|
||||
db_for_write, column.default
|
||||
)
|
||||
table.create(
|
||||
{column.name: column.type for column in columns},
|
||||
pk=pks or pk,
|
||||
foreign_keys=create_request.foreign_keys,
|
||||
not_null=not_null or None,
|
||||
defaults=defaults or None,
|
||||
)
|
||||
return table.schema
|
||||
|
||||
|
|
|
|||
|
|
@ -1968,7 +1968,14 @@ To create a table, make a ``POST`` to ``/<database>/-/create``. This requires th
|
|||
},
|
||||
{
|
||||
"name": "title",
|
||||
"type": "text"
|
||||
"type": "text",
|
||||
"not_null": true,
|
||||
"default": "Untitled"
|
||||
},
|
||||
{
|
||||
"name": "created",
|
||||
"type": "text",
|
||||
"default_expr": "current_timestamp"
|
||||
}
|
||||
],
|
||||
"pk": "id"
|
||||
|
|
@ -1981,6 +1988,9 @@ The JSON here describes the table that will be created:
|
|||
|
||||
- ``name`` is the name of the column. This is required.
|
||||
- ``type`` is the type of the column. This is optional - if not provided, ``text`` will be assumed. The valid types are ``text``, ``integer``, ``float`` and ``blob``.
|
||||
- ``not_null`` can be set to ``true`` to create this column with a ``NOT NULL`` constraint.
|
||||
- ``default`` can be used to set a literal default value for this column.
|
||||
- ``default_expr`` can be used instead of ``default`` to set a SQLite default expression. The supported values are ``current_timestamp``, ``current_date`` and ``current_time``.
|
||||
- ``fk_table`` can be used to create a single-column foreign key constraint referencing another table. ``fk_column`` is optional and can be used to specify the referenced column - if omitted, Datasette will use the single primary key of ``fk_table``.
|
||||
|
||||
* ``pk`` is the primary key for the table. This is optional - if not provided, Datasette will create a SQLite table with a hidden ``rowid`` column.
|
||||
|
|
@ -2028,7 +2038,7 @@ If the table is successfully created this will return a ``201`` status code and
|
|||
"table": "name_of_new_table",
|
||||
"table_url": "http://127.0.0.1:8001/data/name_of_new_table",
|
||||
"table_api_url": "http://127.0.0.1:8001/data/name_of_new_table.json",
|
||||
"schema": "CREATE TABLE [name_of_new_table] (\n [id] INTEGER PRIMARY KEY,\n [title] TEXT\n)"
|
||||
"schema": "CREATE TABLE [name_of_new_table] (\n [id] INTEGER PRIMARY KEY,\n [title] TEXT NOT NULL DEFAULT 'Untitled',\n [created] TEXT DEFAULT CURRENT_TIMESTAMP\n)"
|
||||
}
|
||||
|
||||
.. _TableCreateView_example:
|
||||
|
|
|
|||
|
|
@ -1935,6 +1935,60 @@ async def test_create_table_with_foreign_key(ds_write):
|
|||
assert "[owner_id] INTEGER REFERENCES [owners]([id])" in data["schema"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_table_with_column_constraints(ds_write):
|
||||
token = write_token(ds_write)
|
||||
response = await ds_write.client.post(
|
||||
"/data/-/create",
|
||||
json={
|
||||
"table": "constrained",
|
||||
"columns": [
|
||||
{"name": "id", "type": "integer"},
|
||||
{
|
||||
"name": "title",
|
||||
"type": "text",
|
||||
"not_null": True,
|
||||
"default": "Untitled",
|
||||
},
|
||||
{
|
||||
"name": "created",
|
||||
"type": "text",
|
||||
"default_expr": "current_timestamp",
|
||||
},
|
||||
{"name": "score", "type": "integer", "default": 0},
|
||||
{"name": "literal_default", "type": "text", "default": "hello)"},
|
||||
],
|
||||
"pk": "id",
|
||||
},
|
||||
headers=_headers(token),
|
||||
)
|
||||
assert response.status_code == 201, response.text
|
||||
data = response.json()
|
||||
assert data["ok"] is True
|
||||
assert "NOT NULL DEFAULT 'Untitled'" in data["schema"]
|
||||
assert "DEFAULT CURRENT_TIMESTAMP" in data["schema"]
|
||||
assert "DEFAULT 0" in data["schema"]
|
||||
assert "DEFAULT 'hello)'" in data["schema"]
|
||||
|
||||
db = ds_write.get_database("data")
|
||||
columns = (
|
||||
await db.execute("select * from pragma_table_info('constrained') order by cid")
|
||||
).dicts()
|
||||
assert [column["name"] for column in columns] == [
|
||||
"id",
|
||||
"title",
|
||||
"created",
|
||||
"score",
|
||||
"literal_default",
|
||||
]
|
||||
assert columns[0]["pk"] == 1
|
||||
assert columns[1]["notnull"] == 1
|
||||
assert columns[1]["dflt_value"] == "'Untitled'"
|
||||
assert columns[2]["dflt_value"] == "CURRENT_TIMESTAMP"
|
||||
assert columns[3]["dflt_value"] == "0"
|
||||
assert columns[4]["dflt_value"] == "'hello)'"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"column,expected_error",
|
||||
|
|
@ -1947,9 +2001,26 @@ async def test_create_table_with_foreign_key(ds_write):
|
|||
{"name": "owner_id", "type": "integer", "fk_column": "id"},
|
||||
"columns.0: fk_column requires fk_table",
|
||||
),
|
||||
(
|
||||
{
|
||||
"name": "created",
|
||||
"type": "text",
|
||||
"default_expr": "datetime('now')",
|
||||
},
|
||||
"columns.0.default_expr: Input should be 'current_timestamp', 'current_date' or 'current_time'",
|
||||
),
|
||||
(
|
||||
{
|
||||
"name": "created",
|
||||
"type": "text",
|
||||
"default": "x",
|
||||
"default_expr": "current_timestamp",
|
||||
},
|
||||
"columns.0: Value error, default and default_expr cannot both be provided",
|
||||
),
|
||||
),
|
||||
)
|
||||
async def test_create_table_foreign_key_validation(ds_write, column, expected_error):
|
||||
async def test_create_table_column_validation(ds_write, column, expected_error):
|
||||
token = write_token(ds_write)
|
||||
response = await ds_write.client.post(
|
||||
"/data/-/create",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue