current_unixtime and current_unixtime_ms default_expr options

Plus tweaked how alter table changing those works a bit.
This commit is contained in:
Simon Willison 2026-06-22 13:42:35 -07:00
commit b932d0dc78
7 changed files with 521 additions and 54 deletions

View file

@ -109,14 +109,64 @@ function createCustomColumnTypeSelect(options, className, placeholderClass) {
return select;
}
var DEFAULT_EXPRESSION_LABELS = {
current_timestamp: "Current timestamp in UTC, e.g. 2026-05-01 13:34:00",
current_date: "Current date in UTC, e.g. 2026-05-01",
current_time: "Current time in UTC, e.g. 13:34:00",
};
function normalizeDefaultExpressionOption(option) {
if (typeof option === "string") {
return {
value: option,
label: option.replace(/_/g, " "),
sqliteType: "",
};
}
option = option || {};
return {
value: option.value || "",
label: option.label || option.value || "",
sqliteType: option.sqliteType || "",
};
}
function defaultExpressionLabel(value) {
return DEFAULT_EXPRESSION_LABELS[value] || value.replace(/_/g, " ");
function defaultExpressionOptionForValue(options, value) {
var match = null;
(options || []).some(function (option) {
var normalized = normalizeDefaultExpressionOption(option);
if (normalized.value === value) {
match = normalized;
return true;
}
return false;
});
return match;
}
function defaultExpressionLabelForValue(options, value) {
var option = defaultExpressionOptionForValue(options, value);
return option && option.label
? option.label
: value
? value.replace(/_/g, " ")
: "";
}
function applyDefaultExpressionColumnType(row, prefix, options, columnTypes) {
var defaultExprSelect = row.querySelector("." + prefix + "-default-expr");
var typeSelect = row.querySelector("." + prefix + "-column-type");
if (!defaultExprSelect || !typeSelect || !defaultExprSelect.value) {
return false;
}
var option = defaultExpressionOptionForValue(
options,
defaultExprSelect.value,
);
if (
option &&
option.sqliteType &&
(columnTypes || []).indexOf(option.sqliteType) !== -1 &&
typeSelect.value !== option.sqliteType
) {
typeSelect.value = option.sqliteType;
return true;
}
return false;
}
function createDefaultExpressionSelect(
@ -132,9 +182,13 @@ function createDefaultExpressionSelect(
blankOption.textContent = "- default expr -";
select.appendChild(blankOption);
options.forEach(function (option) {
var normalized = normalizeDefaultExpressionOption(option);
if (!normalized.value) {
return;
}
var optionElement = document.createElement("option");
optionElement.value = option;
optionElement.textContent = defaultExpressionLabel(option);
optionElement.value = normalized.value;
optionElement.textContent = normalized.label;
select.appendChild(optionElement);
});
select.value = value || "";
@ -1078,6 +1132,18 @@ function createTableColumnRow(state, column) {
if (defaultControls.defaultExprSelect.value) {
defaultControls.defaultInput.value = "";
}
if (
applyDefaultExpressionColumnType(
row,
"table-create",
tableCreateDefaultExpressions(),
tableCreateColumnTypes(),
)
) {
syncTableCreateCustomTypeForSqliteType(row);
syncTableCreateForeignKeyOptions(row, state);
syncTableCreateCustomTypeAndForeignKey(row, state);
}
syncSchemaDialogDefaultControls(row, "table-create");
clearTableCreateDialogError(state);
});
@ -1948,8 +2014,15 @@ function createTableAlterColumnRow(state, column) {
var originalName = existing ? column.name || "" : "";
var originalCustomType =
existing && column.column_type ? column.column_type.type || "" : "";
var originalDefaultExpr =
existing && column.has_default && column.default_expr
? column.default_expr
: "";
var originalDefault =
existing && column.has_default && column.default !== null
existing &&
column.has_default &&
!originalDefaultExpr &&
column.default !== null
? String(column.default)
: "";
var originalForeignKey =
@ -1965,6 +2038,7 @@ function createTableAlterColumnRow(state, column) {
row.dataset.originalNotNull = existing && column.notnull ? "1" : "0";
row.dataset.originalHasDefault = existing && column.has_default ? "1" : "0";
row.dataset.originalDefault = originalDefault;
row.dataset.originalDefaultExpr = originalDefaultExpr;
row.dataset.originalPk = existing && column.is_pk ? "1" : "0";
row.dataset.originalCustomType = originalCustomType;
row.dataset.originalForeignKey = originalForeignKey;
@ -2051,7 +2125,7 @@ function createTableAlterColumnRow(state, column) {
tableAlterDefaultExpressions(),
{
defaultValue: originalDefault,
defaultExpr: "",
defaultExpr: originalDefaultExpr,
},
);
var defaultInput = defaultControls.defaultInput;
@ -2167,6 +2241,18 @@ function createTableAlterColumnRow(state, column) {
if (defaultExprSelect.value) {
defaultInput.value = "";
}
if (
applyDefaultExpressionColumnType(
row,
"table-alter",
tableAlterDefaultExpressions(),
tableAlterColumnTypes(),
)
) {
syncTableAlterCustomTypeForSqliteType(row);
syncTableAlterForeignKeyOptions(row, state);
syncSchemaDialogCustomTypeAndForeignKey(row, state, "table-alter");
}
syncSchemaDialogDefaultControls(row, "table-alter");
updateTableAlterSaveButtonState(state);
});
@ -2336,6 +2422,7 @@ function collectTableAlterRows(state) {
signature.originalNotNull = row.dataset.originalNotNull === "1";
signature.originalHasDefault = row.dataset.originalHasDefault === "1";
signature.originalDefault = row.dataset.originalDefault || "";
signature.originalDefaultExpr = row.dataset.originalDefaultExpr || "";
signature.originalPk = row.dataset.originalPk === "1";
signature.originalCustomType = row.dataset.originalCustomType || "";
signature.originalForeignKey = row.dataset.originalForeignKey || "";
@ -2567,8 +2654,12 @@ function collectTableAlterPayload(state) {
if (row.notNull !== row.originalNotNull) {
alterArgs.not_null = row.notNull;
}
if (row.defaultExpr) {
alterArgs.default_expr = row.defaultExpr;
if (row.defaultExpr !== row.originalDefaultExpr) {
if (row.defaultExpr) {
alterArgs.default_expr = row.defaultExpr;
} else {
alterArgs.default = row.defaultValue === "" ? null : row.defaultValue;
}
} else if (row.originalHasDefault) {
if (row.defaultValue !== row.originalDefault) {
alterArgs.default = row.defaultValue === "" ? null : row.defaultValue;
@ -2630,7 +2721,7 @@ function tableAlterQuotedName(name) {
}
function tableAlterReadableDefaultExpression(value) {
return value ? value.replace(/_/g, " ") : "";
return defaultExpressionLabelForValue(tableAlterDefaultExpressions(), value);
}
function tableAlterReadableValue(value) {

View file

@ -51,8 +51,9 @@ from .database import QueryView
from .table_create_alter import (
ALTER_TABLE_COLUMN_TYPES,
ALTER_TABLE_TYPE_FOR_SQLITE_TYPE,
DEFAULT_EXPR_SQL,
_custom_column_type_options_for_create_table,
default_expr_for_sql,
default_expression_options,
)
from .table_extras import (
TABLE_EXTRA_BUNDLES,
@ -401,23 +402,25 @@ async def _table_alter_ui(
continue
sqlite_type = SQLiteType.from_declared_type(column.type)
column_type = column_types_map.get(column.name)
columns.append(
{
"name": column.name,
"type": ALTER_TABLE_TYPE_FOR_SQLITE_TYPE.get(sqlite_type, "text"),
"sqlite_type": sqlite_type.value,
"notnull": column.notnull,
"default": column.default_value,
"has_default": column.default_value is not None,
"is_pk": column.name in pks,
"foreign_key": foreign_keys_by_column.get(column.name),
"column_type": (
{"type": column_type.name, "config": column_type.config}
if column_type is not None
else None
),
}
)
default_expr = default_expr_for_sql(column.default_value)
column_data = {
"name": column.name,
"type": ALTER_TABLE_TYPE_FOR_SQLITE_TYPE.get(sqlite_type, "text"),
"sqlite_type": sqlite_type.value,
"notnull": column.notnull,
"default": None if default_expr else column.default_value,
"has_default": column.default_value is not None,
"is_pk": column.name in pks,
"foreign_key": foreign_keys_by_column.get(column.name),
"column_type": (
{"type": column_type.name, "config": column_type.config}
if column_type is not None
else None
),
}
if default_expr:
column_data["default_expr"] = default_expr
columns.append(column_data)
data = {
"path": "{}/-/alter".format(datasette.urls.table(database_name, table_name)),
@ -425,7 +428,7 @@ async def _table_alter_ui(
"columns": columns,
"primaryKeys": pks,
"columnTypes": ALTER_TABLE_COLUMN_TYPES,
"defaultExpressions": list(DEFAULT_EXPR_SQL),
"defaultExpressions": default_expression_options(),
"foreignKeyTargetsPath": "{}/-/foreign-key-targets?table={}".format(
datasette.urls.database(database_name),
urllib.parse.quote(table_name, safe=""),

View file

@ -266,7 +266,7 @@ async def _create_table_ui_context(
),
"databaseName": database_name,
"columnTypes": CREATE_TABLE_COLUMN_TYPES,
"defaultExpressions": list(DEFAULT_EXPR_SQL),
"defaultExpressions": default_expression_options(),
}
can_set_column_type = await datasette.allowed(
action="set-column-type",
@ -308,12 +308,109 @@ def _custom_column_type_options_for_create_table(datasette):
SqliteApiType = Literal["text", "integer", "float", "blob"]
DefaultExpr = Literal["current_timestamp", "current_date", "current_time"]
DEFAULT_EXPR_SQL = {
"current_timestamp": "CURRENT_TIMESTAMP",
"current_date": "CURRENT_DATE",
"current_time": "CURRENT_TIME",
DEFAULT_EXPRESSIONS = {
"current_timestamp": {
"sql": "CURRENT_TIMESTAMP",
"label": "Current timestamp in UTC, e.g. 2026-05-01 13:34:00",
"sqliteType": "text",
},
"current_date": {
"sql": "CURRENT_DATE",
"label": "Current date in UTC, e.g. 2026-05-01",
"sqliteType": "text",
},
"current_time": {
"sql": "CURRENT_TIME",
"label": "Current time in UTC, e.g. 13:34:00",
"sqliteType": "text",
},
"current_unixtime": {
"sql": "(CAST(strftime('%s', 'now') AS INTEGER))",
"label": "Current Unix time, integer seconds since the epoch",
"sqliteType": "integer",
},
"current_unixtime_ms": {
"sql": "(CAST((julianday('now') - 2440587.5) * 86400000 AS INTEGER))",
"label": "Current Unix time, integer milliseconds since the epoch",
"sqliteType": "integer",
},
}
DefaultExpr = str
DEFAULT_EXPR_SQL = {
name: metadata["sql"] for name, metadata in DEFAULT_EXPRESSIONS.items()
}
def _strip_wrapping_parentheses(expression):
expression = expression.strip()
while expression.startswith("(") and expression.endswith(")"):
depth = 0
in_single_quote = False
wraps_whole_expression = True
i = 0
while i < len(expression):
char = expression[i]
if char == "'":
if (
in_single_quote
and i + 1 < len(expression)
and expression[i + 1] == "'"
):
i += 2
continue
in_single_quote = not in_single_quote
elif not in_single_quote:
if char == "(":
depth += 1
elif char == ")":
depth -= 1
if depth == 0 and i != len(expression) - 1:
wraps_whole_expression = False
break
i += 1
if not wraps_whole_expression or depth != 0 or in_single_quote:
break
expression = expression[1:-1].strip()
return expression
def _default_expression_lookup_key(expression):
return re.sub(r"\s+", " ", _strip_wrapping_parentheses(expression)).lower()
DEFAULT_EXPR_BY_SQL = {
_default_expression_lookup_key(sql): name for name, sql in DEFAULT_EXPR_SQL.items()
}
def default_expr_for_sql(expression):
if expression is None:
return None
return DEFAULT_EXPR_BY_SQL.get(_default_expression_lookup_key(expression))
def _quoted_options(options):
if len(options) == 1:
return "'{}'".format(options[0])
return "{} or '{}'".format(
", ".join("'{}'".format(option) for option in options[:-1]),
options[-1],
)
def _default_expr_error_message():
return "Input should be {}".format(_quoted_options(list(DEFAULT_EXPRESSIONS)))
def default_expression_options():
return [
{
"value": value,
"label": metadata["label"],
"sqliteType": metadata["sqliteType"],
}
for value, metadata in DEFAULT_EXPRESSIONS.items()
]
class _StrictPydanticModel(BaseModel):
@ -324,6 +421,13 @@ class _DefaultArgsMixin(_StrictPydanticModel):
default: Any | None = None
default_expr: DefaultExpr | None = None
@field_validator("default_expr")
@classmethod
def validate_default_expr_value(cls, value):
if value is not None and value not in DEFAULT_EXPRESSIONS:
raise PydanticCustomError("default_expr", _default_expr_error_message())
return value
@model_validator(mode="after")
def validate_default_fields(self):
has_default = "default" in self.model_fields_set

View file

@ -44,7 +44,7 @@ looks like this:
``"ok"`` is always ``true`` if an error did not occur.
The ``"rows"`` key is a list of objects, each one representing a row.
The ``"rows"`` key is a list of objects, each one representing a row.
The ``"truncated"`` key lets you know if the query was truncated. This can happen if a SQL query returns more than 1,000 results (or the :ref:`setting_max_returned_rows` setting).
@ -1990,7 +1990,7 @@ The JSON here describes the table that will be created:
- ``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``.
- ``default_expr`` can be used instead of ``default`` to set a SQLite default expression. See :ref:`default_expr values <json_api_default_expr_values>`.
- ``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.
@ -2004,6 +2004,32 @@ The JSON here describes the table that will be created:
* ``replace`` can be set to ``true`` to replace existing rows by primary key if the table already exists. This requires the :ref:`actions_update_row` permission.
* ``alter`` can be set to ``true`` if you want to automatically add any missing columns to the table. This requires the :ref:`actions_alter_table` permission.
.. _json_api_default_expr_values:
``default_expr`` accepts these values:
.. list-table::
:header-rows: 1
* - Value
- Recommended column type
- Example inserted value
* - ``current_timestamp``
- ``text``
- ``2026-05-01 13:34:00``
* - ``current_date``
- ``text``
- ``2026-05-01``
* - ``current_time``
- ``text``
- ``13:34:00``
* - ``current_unixtime``
- ``integer``
- ``1777642440``
* - ``current_unixtime_ms``
- ``integer``
- ``1777642440000``
This example creates a foreign key from ``projects.owner_id`` to the single primary key of ``owners``:
.. code-block:: json
@ -2314,7 +2340,7 @@ Supported operations:
* ``set_foreign_keys`` replaces all foreign key constraints on the table. ``args`` accepts ``foreign_keys``, a list of objects that each have ``column``, ``fk_table`` and optional ``fk_column``. An empty list removes all foreign key constraints.
* ``reorder_columns`` reorders columns. ``args`` accepts ``columns``, a list of one or more column names. Columns omitted from this list will appear afterwards in their existing order.
``default`` is always treated as a literal value. ``default_expr`` accepts one of ``current_timestamp``, ``current_date`` or ``current_time`` and is rendered as the corresponding SQLite default expression.
``default`` is always treated as a literal value. ``default_expr`` accepts the values shown in :ref:`default_expr values <json_api_default_expr_values>` and is rendered as the corresponding SQLite default expression.
For foreign key operations that omit ``fk_column``, the referenced ``fk_table`` must have a single-column primary key. Datasette will return an error if it cannot identify a single primary key column for that table.

View file

@ -899,6 +899,64 @@ async def test_alter_table_operations(ds_write):
assert event.after_schema == data["schema"]
@pytest.mark.asyncio
@pytest.mark.parametrize(
"default_expr,minimum_value,expected_schema",
(
(
"current_unixtime",
1_600_000_000,
"strftime('%s', 'now')",
),
(
"current_unixtime_ms",
1_600_000_000_000,
"julianday('now')",
),
),
)
async def test_alter_table_integer_default_expr(
ds_write, default_expr, minimum_value, expected_schema
):
token = write_token(ds_write, permissions=["at"])
db = ds_write.get_database("data")
response = await ds_write.client.post(
"/data/docs/-/alter",
json={
"operations": [
{
"op": "add_column",
"args": {
"name": "created",
"type": "integer",
"default_expr": default_expr,
},
}
]
},
headers=_headers(token),
)
assert response.status_code == 200, response.text
data = response.json()
assert expected_schema in data["schema"]
columns = await db.execute("select * from pragma_table_info('docs')")
created_column = [
column for column in columns.dicts() if column["name"] == "created"
][0]
assert created_column["type"] == "INTEGER"
assert expected_schema in created_column["dflt_value"]
row = await db.execute_write_fn(
lambda conn: conn.execute(
"insert into docs (title) values ('with default') "
"returning created, typeof(created)"
).fetchone()
)
assert row[0] > minimum_value
assert row[1] == "integer"
@pytest.mark.asyncio
async def test_alter_table_rename_table(ds_write):
token = write_token(ds_write, permissions=["at"])
@ -1315,7 +1373,7 @@ async def test_alter_table_permission_denied(ds_write):
}
]
},
"operations.0.add_column.args.default_expr: Input should be 'current_timestamp', 'current_date' or 'current_time'",
"operations.0.add_column.args.default_expr: Input should be 'current_timestamp', 'current_date', 'current_time', 'current_unixtime' or 'current_unixtime_ms'",
),
(
{
@ -2053,6 +2111,63 @@ async def test_create_table_with_column_constraints(ds_write):
assert columns[4]["dflt_value"] == "'hello)'"
@pytest.mark.asyncio
@pytest.mark.parametrize(
"default_expr,minimum_value,expected_schema",
(
(
"current_unixtime",
1_600_000_000,
"strftime('%s', 'now')",
),
(
"current_unixtime_ms",
1_600_000_000_000,
"julianday('now')",
),
),
)
async def test_create_table_integer_default_expr(
ds_write, default_expr, minimum_value, expected_schema
):
token = write_token(ds_write)
table = "default_{}".format(default_expr)
response = await ds_write.client.post(
"/data/-/create",
json={
"table": table,
"columns": [
{"name": "id", "type": "integer"},
{
"name": "created",
"type": "integer",
"default_expr": default_expr,
},
],
"pk": "id",
},
headers=_headers(token),
)
assert response.status_code == 201, response.text
data = response.json()
assert expected_schema in data["schema"]
db = ds_write.get_database("data")
columns = (await db.execute("select * from pragma_table_info(?)", [table])).dicts()
assert columns[1]["type"] == "INTEGER"
assert expected_schema in columns[1]["dflt_value"]
row = await db.execute_write_fn(
lambda conn: conn.execute(
"insert into [{}] default values returning created, typeof(created)".format(
table
)
).fetchone()
)
assert row[0] > minimum_value
assert row[1] == "integer"
@pytest.mark.asyncio
@pytest.mark.parametrize(
"column,expected_error",
@ -2071,7 +2186,7 @@ async def test_create_table_with_column_constraints(ds_write):
"type": "text",
"default_expr": "datetime('now')",
},
"columns.0.default_expr: Input should be 'current_timestamp', 'current_date' or 'current_time'",
"columns.0.default_expr: Input should be 'current_timestamp', 'current_date', 'current_time', 'current_unixtime' or 'current_unixtime_ms'",
),
(
{

View file

@ -98,6 +98,10 @@ def write_playwright_database(db_path):
notes text,
score integer default 5
);
create table defaults_demo (
id integer primary key,
created_ms integer default (CAST((julianday('now') - 2440587.5) * 86400000 AS INTEGER))
);
insert into projects (title, metadata, logo, notes, score) values
(
'Build Datasette',
@ -136,6 +140,11 @@ def write_playwright_config(config_path):
"delete-row": True,
},
},
"defaults_demo": {
"permissions": {
"alter-table": True,
},
},
},
},
},
@ -394,6 +403,32 @@ def test_create_table_foreign_key_selection_updates_column_type(page, datasette_
assert foreign_key_select.input_value() == "projects\u001fid"
@pytest.mark.playwright
def test_create_table_unix_default_expression_updates_column_type(
page, datasette_server
):
page.goto(f"{datasette_server}data")
page.locator("details.actions-menu-links summary").click()
page.locator('button[data-database-action="create-table"]').click()
dialog = page.locator("#table-create-dialog")
dialog.wait_for()
row = dialog.locator(".table-create-column-row").nth(1)
row.locator(".table-create-more-options").click()
row.locator(".table-create-default-options summary").click()
type_select = row.locator(".table-create-column-type")
default_expr = row.locator(".table-create-default-expr")
assert type_select.input_value() == "text"
assert (
"Current Unix time, integer milliseconds since the epoch"
in default_expr.locator("option").last.inner_text()
)
default_expr.select_option("current_unixtime_ms")
assert type_select.input_value() == "integer"
@pytest.mark.playwright
def test_alter_table_foreign_key_selection_updates_blank_column(page, datasette_server):
page.goto(f"{datasette_server}data/projects")
@ -416,6 +451,52 @@ def test_alter_table_foreign_key_selection_updates_blank_column(page, datasette_
assert foreign_key_select.input_value() == "projects\u001fid"
@pytest.mark.playwright
def test_alter_table_unix_default_expression_updates_column_type(
page, datasette_server
):
page.goto(f"{datasette_server}data/projects")
page.locator("details.actions-menu-links summary").click()
page.locator('button[data-table-action="alter-table"]').click()
dialog = page.locator("#table-alter-dialog")
dialog.wait_for()
dialog.locator(".table-alter-add-column").click()
row = dialog.locator(".table-alter-column-row").last
row.locator(".table-alter-default-options summary").click()
type_select = row.locator(".table-alter-column-type")
default_expr = row.locator(".table-alter-default-expr")
assert type_select.input_value() == "text"
assert (
"Current Unix time, integer seconds since the epoch"
in default_expr.locator("option").all_inner_texts()
)
default_expr.select_option("current_unixtime")
assert type_select.input_value() == "integer"
@pytest.mark.playwright
def test_alter_table_existing_default_expression_populates_select(
page, datasette_server
):
page.goto(f"{datasette_server}data/defaults_demo")
page.locator("details.actions-menu-links summary").click()
page.locator('button[data-table-action="alter-table"]').click()
dialog = page.locator("#table-alter-dialog")
dialog.wait_for()
row = dialog.locator(".table-alter-column-row").nth(1)
row.locator(".table-alter-more-options").click()
row.locator(".table-alter-default-options summary").click()
assert row.locator(".table-alter-default-expr").input_value() == (
"current_unixtime_ms"
)
assert row.locator(".table-alter-default").input_value() == ""
@pytest.mark.playwright
def test_alter_table_flow(page, datasette_server):
page.goto(f"{datasette_server}data/projects")

View file

@ -40,6 +40,35 @@ def database_data_from_soup(soup):
return json.loads(match.group(1))
DEFAULT_EXPRESSION_OPTIONS = [
{
"value": "current_timestamp",
"label": "Current timestamp in UTC, e.g. 2026-05-01 13:34:00",
"sqliteType": "text",
},
{
"value": "current_date",
"label": "Current date in UTC, e.g. 2026-05-01",
"sqliteType": "text",
},
{
"value": "current_time",
"label": "Current time in UTC, e.g. 13:34:00",
"sqliteType": "text",
},
{
"value": "current_unixtime",
"label": "Current Unix time, integer seconds since the epoch",
"sqliteType": "integer",
},
{
"value": "current_unixtime_ms",
"label": "Current Unix time, integer milliseconds since the epoch",
"sqliteType": "integer",
},
]
@pytest.mark.asyncio
@pytest.mark.parametrize(
"path,expected_definition_sql",
@ -997,11 +1026,7 @@ async def test_database_create_table_action_button_and_data():
"foreignKeyTargetsPath": "/data/-/foreign-key-targets",
"databaseName": "data",
"columnTypes": ["text", "integer", "float", "blob"],
"defaultExpressions": [
"current_timestamp",
"current_date",
"current_time",
],
"defaultExpressions": DEFAULT_EXPRESSION_OPTIONS,
},
}
assert "customColumnTypes" not in database_data_from_soup(soup)["createTable"]
@ -1113,7 +1138,9 @@ async def test_table_alter_action_button_and_data():
create table items (
id integer primary key,
name text not null,
score integer default 5
score integer default 5,
created text default current_timestamp,
created_ms integer default (CAST((julianday('now') - 2440587.5) * 86400000 AS INTEGER))
);
""")
response = await ds.client.get("/data/items", actor={"id": "root"})
@ -1145,11 +1172,7 @@ async def test_table_alter_action_button_and_data():
assert alter_data["foreignKeyTargetsPath"] == (
"/data/-/foreign-key-targets?table=items"
)
assert alter_data["defaultExpressions"] == [
"current_timestamp",
"current_date",
"current_time",
]
assert alter_data["defaultExpressions"] == DEFAULT_EXPRESSION_OPTIONS
assert [option["name"] for option in alter_data["customColumnTypes"]] == [
"email",
"json",
@ -1191,6 +1214,30 @@ async def test_table_alter_action_button_and_data():
"foreign_key": None,
"column_type": None,
},
{
"name": "created",
"type": "text",
"sqlite_type": "TEXT",
"notnull": 0,
"default": None,
"default_expr": "current_timestamp",
"has_default": True,
"is_pk": False,
"foreign_key": None,
"column_type": None,
},
{
"name": "created_ms",
"type": "integer",
"sqlite_type": "INTEGER",
"notnull": 0,
"default": None,
"default_expr": "current_unixtime_ms",
"has_default": True,
"is_pk": False,
"foreign_key": None,
"column_type": None,
},
]
response_without_permission = await ds.client.get(