2017-12-15 04:04:17 -08:00
|
|
|
|
from datasette.app import Datasette
|
2022-11-03 13:36:11 -07:00
|
|
|
|
from datasette.utils.sqlite import sqlite3
|
2020-08-11 17:24:40 -07:00
|
|
|
|
from datasette.utils.testing import TestClient
|
2020-06-09 12:57:54 -07:00
|
|
|
|
import click
|
2020-06-07 14:14:10 -07:00
|
|
|
|
import contextlib
|
2018-03-29 22:10:09 -07:00
|
|
|
|
import itertools
|
2018-06-17 12:16:04 -07:00
|
|
|
|
import json
|
2017-12-15 04:04:17 -08:00
|
|
|
|
import os
|
2019-05-05 22:50:33 -04:00
|
|
|
|
import pathlib
|
2018-05-31 06:40:30 -07:00
|
|
|
|
import pytest
|
2018-04-08 17:06:10 -07:00
|
|
|
|
import random
|
2018-03-29 22:10:09 -07:00
|
|
|
|
import string
|
2017-12-15 04:04:17 -08:00
|
|
|
|
import tempfile
|
2020-04-02 17:54:27 -07:00
|
|
|
|
import textwrap
|
2017-12-15 04:04:17 -08:00
|
|
|
|
|
2019-07-03 22:36:44 -07:00
|
|
|
|
# This temp file is used by one of the plugin config tests
|
|
|
|
|
|
TEMP_PLUGIN_SECRET_FILE = os.path.join(tempfile.gettempdir(), "plugin-secret")
|
|
|
|
|
|
|
2020-05-27 17:57:25 -07:00
|
|
|
|
PLUGINS_DIR = str(pathlib.Path(__file__).parent / "plugins")
|
|
|
|
|
|
|
2020-06-05 16:46:37 -07:00
|
|
|
|
EXPECTED_PLUGINS = [
|
|
|
|
|
|
{
|
|
|
|
|
|
"name": "messages_output_renderer.py",
|
|
|
|
|
|
"static": False,
|
|
|
|
|
|
"templates": False,
|
|
|
|
|
|
"version": None,
|
|
|
|
|
|
"hooks": ["register_output_renderer"],
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
"name": "my_plugin.py",
|
|
|
|
|
|
"static": False,
|
|
|
|
|
|
"templates": False,
|
|
|
|
|
|
"version": None,
|
|
|
|
|
|
"hooks": [
|
|
|
|
|
|
"actor_from_request",
|
2020-06-18 11:37:28 -07:00
|
|
|
|
"asgi_wrapper",
|
2020-06-18 16:22:33 -07:00
|
|
|
|
"canned_queries",
|
2020-11-02 10:27:25 -08:00
|
|
|
|
"database_actions",
|
2020-06-05 16:46:37 -07:00
|
|
|
|
"extra_body_script",
|
|
|
|
|
|
"extra_css_urls",
|
|
|
|
|
|
"extra_js_urls",
|
|
|
|
|
|
"extra_template_vars",
|
2020-06-30 21:17:38 -07:00
|
|
|
|
"forbidden",
|
2024-03-12 13:44:07 -07:00
|
|
|
|
"homepage_actions",
|
2020-10-29 20:45:15 -07:00
|
|
|
|
"menu_links",
|
2025-10-24 14:31:33 -07:00
|
|
|
|
"permission_resources_sql",
|
2020-06-05 16:46:37 -07:00
|
|
|
|
"prepare_connection",
|
|
|
|
|
|
"prepare_jinja2_environment",
|
2024-02-27 21:55:16 -08:00
|
|
|
|
"query_actions",
|
2025-10-23 15:21:55 -07:00
|
|
|
|
"register_actions",
|
2020-06-05 16:46:37 -07:00
|
|
|
|
"register_facet_classes",
|
2020-06-27 19:58:16 -07:00
|
|
|
|
"register_magic_parameters",
|
2020-06-08 20:12:06 -07:00
|
|
|
|
"register_routes",
|
register_token_handler() plugin hook for custom API token backends (#2650)
Closes #2649
* Add register_token_handler plugin hook for pluggable token backends
Adds a new register_token_handler hook that allows plugins to provide
custom token creation and verification backends. This enables plugins
like datasette-oauth to issue tokens without depending on specific
backend plugins like datasette-auth-tokens.
Key changes:
- New datasette/tokens.py with TokenHandler base class and SignedTokenHandler
(the default signed-token implementation moved here)
- New register_token_handler hookspec in hookspecs.py
- Datasette.create_token() is now async and delegates to token handlers
- New Datasette.verify_token() method tries all handlers in sequence
- handler= parameter on create_token() to select a specific backend
- TokenHandler exported from datasette package for plugin use
- Fixed actor_from_request loop to await all coroutines (avoids warnings)
* Add documentation and hook test for register_token_handler
Fixes CI failures: the new hook needs a section in docs/plugin_hooks.rst
(checked by test_plugin_hooks_are_documented) and a test_hook_* function
in test_plugins.py (checked by test_plugin_hooks_have_tests).
* Register tokens module as separate default plugin
Instead of re-exporting hookimpls from default_permissions/__init__.py,
register datasette.default_permissions.tokens as its own DEFAULT_PLUGINS
entry. Cleaner and avoids confusing import-for-side-effect patterns.
* Replace restrict_x params with TokenRestrictions dataclass
Consolidates the three separate restrict_all, restrict_database, and
restrict_resource parameters into a single TokenRestrictions dataclass.
Cleaner API surface for both Datasette.create_token() and
TokenHandler.create_token().
Also clarifies docs re: default handler selection via pluggy ordering.
* Add builder methods to TokenRestrictions
Adds allow_all(), allow_database(), and allow_resource() methods that
return self for chaining. Callers no longer need to manipulate nested
dicts directly:
restrictions = (TokenRestrictions()
.allow_all("view-instance")
.allow_database("mydb", "create-table")
.allow_resource("mydb", "mytable", "insert-row"))
* docs: add 1.0a25 upgrade guide section for create_token() signature change
Ref: https://github.com/simonw/datasette/issues/2649#issuecomment-3962639393
* docs: note that create_token() is now async in upgrade guide
* docs: update internals, plugin_hooks, authentication for new token API
- internals.rst: new async create_token() signature with restrictions
and handler params, add TokenRestrictions reference docs
- plugin_hooks.rst: show full create_token signature in TokenHandler
example, note list returns and error cases
- authentication.rst: cross-reference TokenRestrictions from the
restrictions section
* style: apply black formatting to token handler files
* docs: fix RST heading underline length in internals.rst
* tests: add restrictions round-trip and expiration tests for token handler
Covers allow_database/allow_resource builders, _r payload encoding,
and token_expires in verified actors. Coverage 76% -> 90%.
* tests: add test for signed tokens disabled
* fix: add TokenRestrictions TYPE_CHECKING import to fix ruff F821
* docs: regenerate plugins.rst with cog
* docs: reformat code blocks in plugin_hooks.rst with blacken-docs
* docs: add await .verify_token() to internals.rst
* tests: rewrite register_token_handler test to use real plugin handler
Adds a HardcodedTokenHandler to the test plugins dir that creates
tokens like dstok_hardcoded_token_1. The test now exercises creating
tokens via the default handler (which is the plugin's hardcoded one),
by explicitly naming the hardcoded handler, and by explicitly naming
the signed handler -- then verifies each token round-trips correctly.
* tests: clarify test_token_handler_via_http tests the default signed handler
* fix: use handler="signed" explicitly where signed tokens are expected
The HardcodedTokenHandler in my_plugin.py gets globally registered,
so create_token() without a handler name picks it up as the default.
Fix the create-token view, CLI, and tests to explicitly request the
signed handler where they depend on signed token behavior.
* fix: use handler="signed" in test_create_table_permissions
https://claude.ai/code/session_013cQFiDQjYRrRBH2biFfKuS
2026-02-25 16:32:45 -08:00
|
|
|
|
"register_token_handler",
|
2020-06-05 16:46:37 -07:00
|
|
|
|
"render_cell",
|
2024-03-12 16:13:31 -07:00
|
|
|
|
"row_actions",
|
2021-06-23 15:39:52 -07:00
|
|
|
|
"skip_csrf",
|
2020-06-13 10:55:41 -07:00
|
|
|
|
"startup",
|
2020-10-29 22:16:41 -07:00
|
|
|
|
"table_actions",
|
2024-03-12 14:25:07 -07:00
|
|
|
|
"view_actions",
|
2020-06-05 16:46:37 -07:00
|
|
|
|
],
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
"name": "my_plugin_2.py",
|
|
|
|
|
|
"static": False,
|
|
|
|
|
|
"templates": False,
|
|
|
|
|
|
"version": None,
|
|
|
|
|
|
"hooks": [
|
|
|
|
|
|
"actor_from_request",
|
|
|
|
|
|
"asgi_wrapper",
|
2020-06-18 16:22:33 -07:00
|
|
|
|
"canned_queries",
|
2020-06-05 16:46:37 -07:00
|
|
|
|
"extra_js_urls",
|
|
|
|
|
|
"extra_template_vars",
|
2022-07-17 16:24:39 -07:00
|
|
|
|
"handle_exception",
|
2020-10-29 20:45:15 -07:00
|
|
|
|
"menu_links",
|
2022-09-16 20:38:15 -07:00
|
|
|
|
"prepare_jinja2_environment",
|
2021-07-26 16:16:46 -07:00
|
|
|
|
"register_routes",
|
2020-06-05 16:46:37 -07:00
|
|
|
|
"render_cell",
|
2020-06-13 11:15:33 -07:00
|
|
|
|
"startup",
|
2020-10-29 22:16:41 -07:00
|
|
|
|
"table_actions",
|
2020-06-05 16:46:37 -07:00
|
|
|
|
],
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
"name": "register_output_renderer.py",
|
|
|
|
|
|
"static": False,
|
|
|
|
|
|
"templates": False,
|
|
|
|
|
|
"version": None,
|
|
|
|
|
|
"hooks": ["register_output_renderer"],
|
|
|
|
|
|
},
|
2021-12-17 18:19:09 -08:00
|
|
|
|
{
|
|
|
|
|
|
"name": "sleep_sql_function.py",
|
|
|
|
|
|
"static": False,
|
|
|
|
|
|
"templates": False,
|
|
|
|
|
|
"version": None,
|
|
|
|
|
|
"hooks": ["prepare_connection"],
|
|
|
|
|
|
},
|
2020-06-05 16:46:37 -07:00
|
|
|
|
{
|
|
|
|
|
|
"name": "view_name.py",
|
|
|
|
|
|
"static": False,
|
|
|
|
|
|
"templates": False,
|
|
|
|
|
|
"version": None,
|
|
|
|
|
|
"hooks": ["extra_template_vars"],
|
|
|
|
|
|
},
|
|
|
|
|
|
]
|
|
|
|
|
|
|
2019-07-03 22:36:44 -07:00
|
|
|
|
|
2020-06-07 14:14:10 -07:00
|
|
|
|
@contextlib.contextmanager
|
2018-11-20 09:16:31 -08:00
|
|
|
|
def make_app_client(
|
2018-06-23 17:59:37 -07:00
|
|
|
|
sql_time_limit_ms=None,
|
|
|
|
|
|
max_returned_rows=None,
|
|
|
|
|
|
cors=False,
|
2019-03-31 19:04:17 -07:00
|
|
|
|
memory=False,
|
2021-08-12 18:10:36 -07:00
|
|
|
|
settings=None,
|
2018-06-23 17:59:37 -07:00
|
|
|
|
filename="fixtures.db",
|
2019-03-31 16:51:52 -07:00
|
|
|
|
is_immutable=False,
|
2019-05-14 08:46:57 -07:00
|
|
|
|
extra_databases=None,
|
2019-05-16 07:30:32 -07:00
|
|
|
|
inspect_data=None,
|
2019-06-23 20:13:09 -07:00
|
|
|
|
static_mounts=None,
|
2019-07-02 20:06:22 -07:00
|
|
|
|
template_dir=None,
|
2023-09-13 14:06:25 -07:00
|
|
|
|
config=None,
|
2020-06-03 08:16:50 -07:00
|
|
|
|
metadata=None,
|
2021-02-18 14:09:12 -08:00
|
|
|
|
crossdb=False,
|
2018-06-23 17:59:37 -07:00
|
|
|
|
):
|
2017-12-15 04:04:17 -08:00
|
|
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
2018-06-21 08:21:09 -07:00
|
|
|
|
filepath = os.path.join(tmpdir, filename)
|
2019-05-14 08:46:57 -07:00
|
|
|
|
if is_immutable:
|
|
|
|
|
|
files = []
|
|
|
|
|
|
immutables = [filepath]
|
|
|
|
|
|
else:
|
|
|
|
|
|
files = [filepath]
|
|
|
|
|
|
immutables = []
|
2017-12-15 04:04:17 -08:00
|
|
|
|
conn = sqlite3.connect(filepath)
|
|
|
|
|
|
conn.executescript(TABLES)
|
2019-05-03 12:43:59 -04:00
|
|
|
|
for sql, params in TABLE_PARAMETERIZED_SQL:
|
|
|
|
|
|
with conn:
|
|
|
|
|
|
conn.execute(sql, params)
|
2022-10-25 21:25:47 -07:00
|
|
|
|
# Close the connection to avoid "too many open files" errors
|
|
|
|
|
|
conn.close()
|
2019-05-14 08:46:57 -07:00
|
|
|
|
if extra_databases is not None:
|
|
|
|
|
|
for extra_filename, extra_sql in extra_databases.items():
|
|
|
|
|
|
extra_filepath = os.path.join(tmpdir, extra_filename)
|
2022-10-25 21:25:47 -07:00
|
|
|
|
c2 = sqlite3.connect(extra_filepath)
|
|
|
|
|
|
c2.executescript(extra_sql)
|
|
|
|
|
|
c2.close()
|
2021-06-01 20:03:07 -07:00
|
|
|
|
# Insert at start to help test /-/databases ordering:
|
|
|
|
|
|
files.insert(0, extra_filepath)
|
2017-12-15 04:04:17 -08:00
|
|
|
|
os.chdir(os.path.dirname(filepath))
|
2021-08-12 18:10:36 -07:00
|
|
|
|
settings = settings or {}
|
2021-05-27 09:00:58 -07:00
|
|
|
|
for key, value in {
|
|
|
|
|
|
"default_page_size": 50,
|
|
|
|
|
|
"max_returned_rows": max_returned_rows or 100,
|
|
|
|
|
|
"sql_time_limit_ms": sql_time_limit_ms or 200,
|
|
|
|
|
|
# Default is 3 but this results in "too many open files"
|
|
|
|
|
|
# errors when running the full test suite:
|
|
|
|
|
|
"num_sql_threads": 1,
|
|
|
|
|
|
}.items():
|
2021-08-12 18:10:36 -07:00
|
|
|
|
if key not in settings:
|
|
|
|
|
|
settings[key] = value
|
2017-12-15 04:04:17 -08:00
|
|
|
|
ds = Datasette(
|
2019-05-14 08:46:57 -07:00
|
|
|
|
files,
|
|
|
|
|
|
immutables=immutables,
|
2019-03-31 19:04:17 -07:00
|
|
|
|
memory=memory,
|
2018-06-23 17:59:37 -07:00
|
|
|
|
cors=cors,
|
2020-06-03 08:16:50 -07:00
|
|
|
|
metadata=metadata or METADATA,
|
2023-09-13 14:06:25 -07:00
|
|
|
|
config=config or CONFIG,
|
2020-05-27 17:57:25 -07:00
|
|
|
|
plugins_dir=PLUGINS_DIR,
|
2021-08-12 18:10:36 -07:00
|
|
|
|
settings=settings,
|
2019-05-16 07:30:32 -07:00
|
|
|
|
inspect_data=inspect_data,
|
2019-06-23 20:13:09 -07:00
|
|
|
|
static_mounts=static_mounts,
|
2019-07-02 20:06:22 -07:00
|
|
|
|
template_dir=template_dir,
|
2021-02-18 14:09:12 -08:00
|
|
|
|
crossdb=crossdb,
|
2017-12-15 04:04:17 -08:00
|
|
|
|
)
|
2020-10-09 09:11:24 -07:00
|
|
|
|
yield TestClient(ds)
|
2022-11-03 13:36:11 -07:00
|
|
|
|
# Close as many database connections as possible
|
|
|
|
|
|
# to try and avoid too many open files error
|
|
|
|
|
|
for db in ds.databases.values():
|
|
|
|
|
|
if not db.is_memory:
|
|
|
|
|
|
db.close()
|
2017-12-15 04:04:17 -08:00
|
|
|
|
|
|
|
|
|
|
|
2018-11-20 09:16:31 -08:00
|
|
|
|
@pytest.fixture(scope="session")
|
2019-01-13 14:23:44 -08:00
|
|
|
|
def app_client():
|
2020-06-07 14:14:10 -07:00
|
|
|
|
with make_app_client() as client:
|
|
|
|
|
|
yield client
|
2018-11-20 09:16:31 -08:00
|
|
|
|
|
|
|
|
|
|
|
2019-03-14 16:42:38 -07:00
|
|
|
|
@pytest.fixture(scope="session")
|
|
|
|
|
|
def app_client_no_files():
|
|
|
|
|
|
ds = Datasette([])
|
2020-10-09 09:11:24 -07:00
|
|
|
|
yield TestClient(ds)
|
2019-03-14 16:42:38 -07:00
|
|
|
|
|
2019-05-03 22:15:14 -04:00
|
|
|
|
|
2020-10-24 12:03:24 -07:00
|
|
|
|
@pytest.fixture(scope="session")
|
|
|
|
|
|
def app_client_base_url_prefix():
|
2021-08-12 18:10:36 -07:00
|
|
|
|
with make_app_client(settings={"base_url": "/prefix/"}) as client:
|
2020-10-24 12:03:24 -07:00
|
|
|
|
yield client
|
|
|
|
|
|
|
|
|
|
|
|
|
2019-05-14 08:46:57 -07:00
|
|
|
|
@pytest.fixture(scope="session")
|
|
|
|
|
|
def app_client_two_attached_databases():
|
2020-06-07 14:14:10 -07:00
|
|
|
|
with make_app_client(
|
2019-11-05 00:16:30 +01:00
|
|
|
|
extra_databases={"extra database.db": EXTRA_DATABASE_SQL}
|
2020-06-07 14:14:10 -07:00
|
|
|
|
) as client:
|
|
|
|
|
|
yield client
|
2019-05-14 08:46:57 -07:00
|
|
|
|
|
|
|
|
|
|
|
2021-02-18 14:09:12 -08:00
|
|
|
|
@pytest.fixture(scope="session")
|
|
|
|
|
|
def app_client_two_attached_databases_crossdb_enabled():
|
|
|
|
|
|
with make_app_client(
|
|
|
|
|
|
extra_databases={"extra database.db": EXTRA_DATABASE_SQL},
|
|
|
|
|
|
crossdb=True,
|
|
|
|
|
|
) as client:
|
|
|
|
|
|
yield client
|
|
|
|
|
|
|
|
|
|
|
|
|
2019-10-18 15:51:07 -07:00
|
|
|
|
@pytest.fixture(scope="session")
|
|
|
|
|
|
def app_client_conflicting_database_names():
|
2020-06-07 14:14:10 -07:00
|
|
|
|
with make_app_client(
|
2019-10-18 15:51:07 -07:00
|
|
|
|
extra_databases={"foo.db": EXTRA_DATABASE_SQL, "foo-bar.db": EXTRA_DATABASE_SQL}
|
2020-06-07 14:14:10 -07:00
|
|
|
|
) as client:
|
|
|
|
|
|
yield client
|
2019-10-18 15:51:07 -07:00
|
|
|
|
|
|
|
|
|
|
|
2019-05-16 07:49:34 -07:00
|
|
|
|
@pytest.fixture(scope="session")
|
|
|
|
|
|
def app_client_two_attached_databases_one_immutable():
|
2020-06-07 14:14:10 -07:00
|
|
|
|
with make_app_client(
|
2019-11-05 00:16:30 +01:00
|
|
|
|
is_immutable=True, extra_databases={"extra database.db": EXTRA_DATABASE_SQL}
|
2020-06-07 14:14:10 -07:00
|
|
|
|
) as client:
|
|
|
|
|
|
yield client
|
2019-05-16 07:49:34 -07:00
|
|
|
|
|
|
|
|
|
|
|
2021-06-05 13:15:58 -07:00
|
|
|
|
@pytest.fixture(scope="session")
|
|
|
|
|
|
def app_client_with_trace():
|
2021-08-12 18:10:36 -07:00
|
|
|
|
with make_app_client(settings={"trace_debug": True}, is_immutable=True) as client:
|
2021-06-05 13:15:58 -07:00
|
|
|
|
yield client
|
|
|
|
|
|
|
|
|
|
|
|
|
2018-05-31 06:40:30 -07:00
|
|
|
|
@pytest.fixture(scope="session")
|
2018-05-05 19:41:37 -03:00
|
|
|
|
def app_client_shorter_time_limit():
|
2020-06-07 14:14:10 -07:00
|
|
|
|
with make_app_client(20) as client:
|
|
|
|
|
|
yield client
|
2018-04-17 18:08:51 -07:00
|
|
|
|
|
|
|
|
|
|
|
2018-05-31 06:40:30 -07:00
|
|
|
|
@pytest.fixture(scope="session")
|
|
|
|
|
|
def app_client_returned_rows_matches_page_size():
|
2020-06-07 14:14:10 -07:00
|
|
|
|
with make_app_client(max_returned_rows=50) as client:
|
|
|
|
|
|
yield client
|
2018-04-25 21:04:12 -07:00
|
|
|
|
|
|
|
|
|
|
|
2018-06-04 09:02:07 -07:00
|
|
|
|
@pytest.fixture(scope="session")
|
|
|
|
|
|
def app_client_larger_cache_size():
|
2021-08-12 18:10:36 -07:00
|
|
|
|
with make_app_client(settings={"cache_size_kb": 2500}) as client:
|
2020-06-07 14:14:10 -07:00
|
|
|
|
yield client
|
2018-06-04 09:02:07 -07:00
|
|
|
|
|
|
|
|
|
|
|
2018-06-17 20:21:02 -07:00
|
|
|
|
@pytest.fixture(scope="session")
|
|
|
|
|
|
def app_client_csv_max_mb_one():
|
2021-08-12 18:10:36 -07:00
|
|
|
|
with make_app_client(settings={"max_csv_mb": 1}) as client:
|
2020-06-07 14:14:10 -07:00
|
|
|
|
yield client
|
2018-06-17 20:21:02 -07:00
|
|
|
|
|
|
|
|
|
|
|
2018-06-21 08:21:09 -07:00
|
|
|
|
@pytest.fixture(scope="session")
|
|
|
|
|
|
def app_client_with_dot():
|
2020-06-07 14:14:10 -07:00
|
|
|
|
with make_app_client(filename="fixtures.dot.db") as client:
|
|
|
|
|
|
yield client
|
2018-06-21 08:21:09 -07:00
|
|
|
|
|
|
|
|
|
|
|
2018-06-23 17:59:37 -07:00
|
|
|
|
@pytest.fixture(scope="session")
|
|
|
|
|
|
def app_client_with_cors():
|
2020-10-27 13:39:07 -07:00
|
|
|
|
with make_app_client(is_immutable=True, cors=True) as client:
|
2020-06-07 14:14:10 -07:00
|
|
|
|
yield client
|
2018-06-23 17:59:37 -07:00
|
|
|
|
|
|
|
|
|
|
|
2020-02-25 15:19:29 -05:00
|
|
|
|
@pytest.fixture(scope="session")
|
|
|
|
|
|
def app_client_immutable_and_inspect_file():
|
2020-02-28 17:08:29 -08:00
|
|
|
|
inspect_data = {"fixtures": {"tables": {"sortable": {"count": 100}}}}
|
2020-06-07 14:14:10 -07:00
|
|
|
|
with make_app_client(is_immutable=True, inspect_data=inspect_data) as client:
|
|
|
|
|
|
yield client
|
2020-02-25 15:19:29 -05:00
|
|
|
|
|
|
|
|
|
|
|
2018-03-29 23:26:22 -07:00
|
|
|
|
def generate_compound_rows(num):
|
|
|
|
|
|
for a, b, c in itertools.islice(
|
|
|
|
|
|
itertools.product(string.ascii_lowercase, repeat=3), num
|
|
|
|
|
|
):
|
2020-11-15 15:24:22 -08:00
|
|
|
|
yield a, b, c, f"{a}-{b}-{c}"
|
2018-03-29 23:26:22 -07:00
|
|
|
|
|
|
|
|
|
|
|
2018-04-08 17:06:10 -07:00
|
|
|
|
def generate_sortable_rows(num):
|
|
|
|
|
|
rand = random.Random(42)
|
|
|
|
|
|
for a, b in itertools.islice(
|
|
|
|
|
|
itertools.product(string.ascii_lowercase, repeat=2), num
|
|
|
|
|
|
):
|
|
|
|
|
|
yield {
|
|
|
|
|
|
"pk1": a,
|
|
|
|
|
|
"pk2": b,
|
2020-11-15 15:24:22 -08:00
|
|
|
|
"content": f"{a}-{b}",
|
2018-04-08 17:06:10 -07:00
|
|
|
|
"sortable": rand.randint(-100, 100),
|
|
|
|
|
|
"sortable_with_nulls": rand.choice([None, rand.random(), rand.random()]),
|
|
|
|
|
|
"sortable_with_nulls_2": rand.choice([None, rand.random(), rand.random()]),
|
2018-04-16 18:41:17 -07:00
|
|
|
|
"text": rand.choice(["$null", "$blah"]),
|
2018-04-08 17:06:10 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
2023-09-13 14:06:25 -07:00
|
|
|
|
CONFIG = {
|
2019-07-03 22:36:44 -07:00
|
|
|
|
"plugins": {
|
|
|
|
|
|
"name-of-plugin": {"depth": "root"},
|
|
|
|
|
|
"env-plugin": {"foo": {"$env": "FOO_ENV"}},
|
2020-06-11 17:21:48 -07:00
|
|
|
|
"env-plugin-list": [{"in_a_list": {"$env": "FOO_ENV"}}],
|
2019-07-03 22:36:44 -07:00
|
|
|
|
"file-plugin": {"foo": {"$file": TEMP_PLUGIN_SECRET_FILE}},
|
|
|
|
|
|
},
|
2018-03-27 09:18:32 -07:00
|
|
|
|
"databases": {
|
2018-06-17 11:34:16 -07:00
|
|
|
|
"fixtures": {
|
2018-08-28 01:35:21 -07:00
|
|
|
|
"plugins": {"name-of-plugin": {"depth": "database"}},
|
2018-03-27 09:18:32 -07:00
|
|
|
|
"tables": {
|
|
|
|
|
|
"simple_primary_key": {
|
2018-08-28 03:03:01 -07:00
|
|
|
|
"plugins": {
|
|
|
|
|
|
"name-of-plugin": {
|
|
|
|
|
|
"depth": "table",
|
|
|
|
|
|
"special": "this-is-simple_primary_key",
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
2018-04-08 21:58:25 -07:00
|
|
|
|
},
|
2023-09-13 14:06:25 -07:00
|
|
|
|
"sortable": {
|
|
|
|
|
|
"plugins": {"name-of-plugin": {"depth": "table"}},
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
2023-10-12 09:16:37 -07:00
|
|
|
|
"queries": {
|
|
|
|
|
|
"𝐜𝐢𝐭𝐢𝐞𝐬": "select id, name from facet_cities order by id limit 1;",
|
|
|
|
|
|
"pragma_cache_size": "PRAGMA cache_size;",
|
|
|
|
|
|
"magic_parameters": {
|
|
|
|
|
|
"sql": "select :_header_user_agent as user_agent, :_now_datetime_utc as datetime",
|
|
|
|
|
|
},
|
|
|
|
|
|
"neighborhood_search": {
|
2026-02-17 13:30:24 -08:00
|
|
|
|
"sql": textwrap.dedent("""
|
2023-10-12 09:16:37 -07:00
|
|
|
|
select _neighborhood, facet_cities.name, state
|
|
|
|
|
|
from facetable
|
|
|
|
|
|
join facet_cities
|
|
|
|
|
|
on facetable._city_id = facet_cities.id
|
|
|
|
|
|
where _neighborhood like '%' || :text || '%'
|
|
|
|
|
|
order by _neighborhood;
|
2026-02-17 13:30:24 -08:00
|
|
|
|
"""),
|
2023-10-12 09:16:37 -07:00
|
|
|
|
"title": "Search neighborhoods",
|
|
|
|
|
|
"description_html": "<b>Demonstrating</b> simple like search",
|
|
|
|
|
|
"fragment": "fragment-goes-here",
|
|
|
|
|
|
"hide_sql": True,
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
2023-09-13 14:06:25 -07:00
|
|
|
|
}
|
|
|
|
|
|
},
|
2023-10-12 09:16:37 -07:00
|
|
|
|
"extra_css_urls": ["/static/extra-css-urls.css"],
|
2023-09-13 14:06:25 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
METADATA = {
|
|
|
|
|
|
"title": "Datasette Fixtures",
|
|
|
|
|
|
"description_html": 'An example SQLite database demonstrating Datasette. <a href="/login-as-root">Sign in as root user</a>',
|
|
|
|
|
|
"license": "Apache License 2.0",
|
|
|
|
|
|
"license_url": "https://github.com/simonw/datasette/blob/main/LICENSE",
|
|
|
|
|
|
"source": "tests/fixtures.py",
|
|
|
|
|
|
"source_url": "https://github.com/simonw/datasette/blob/main/tests/fixtures.py",
|
|
|
|
|
|
"about": "About Datasette",
|
|
|
|
|
|
"about_url": "https://github.com/simonw/datasette",
|
|
|
|
|
|
"databases": {
|
|
|
|
|
|
"fixtures": {
|
|
|
|
|
|
"description": "Test tables description",
|
|
|
|
|
|
"tables": {
|
|
|
|
|
|
"simple_primary_key": {
|
|
|
|
|
|
"description_html": "Simple <em>primary</em> key",
|
|
|
|
|
|
"title": "This <em>HTML</em> is escaped",
|
|
|
|
|
|
},
|
2018-04-08 21:58:25 -07:00
|
|
|
|
"sortable": {
|
|
|
|
|
|
"sortable_columns": [
|
|
|
|
|
|
"sortable",
|
|
|
|
|
|
"sortable_with_nulls",
|
|
|
|
|
|
"sortable_with_nulls_2",
|
2018-04-16 18:41:17 -07:00
|
|
|
|
"text",
|
2018-08-28 01:35:21 -07:00
|
|
|
|
],
|
2018-04-08 21:58:25 -07:00
|
|
|
|
},
|
2018-04-14 15:06:52 +01:00
|
|
|
|
"no_primary_key": {"sortable_columns": [], "hidden": True},
|
2018-04-22 13:46:18 -07:00
|
|
|
|
"primary_key_multiple_columns_explicit_label": {
|
2018-04-22 10:51:43 -07:00
|
|
|
|
"label_column": "content2"
|
2018-04-14 15:06:52 +01:00
|
|
|
|
},
|
2018-04-22 13:46:18 -07:00
|
|
|
|
"simple_view": {"sortable_columns": ["content"]},
|
|
|
|
|
|
"searchable_view_configured_by_metadata": {
|
2018-04-22 10:51:43 -07:00
|
|
|
|
"fts_table": "searchable_fts",
|
2019-05-03 22:15:14 -04:00
|
|
|
|
"fts_pk": "pk",
|
2018-04-22 10:51:43 -07:00
|
|
|
|
},
|
2021-08-12 16:53:23 -07:00
|
|
|
|
"roadside_attractions": {
|
|
|
|
|
|
"columns": {
|
|
|
|
|
|
"name": "The name of the attraction",
|
|
|
|
|
|
"address": "The street address for the attraction",
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
2020-03-21 19:28:35 -07:00
|
|
|
|
"attraction_characteristic": {"sort_desc": "pk"},
|
|
|
|
|
|
"facet_cities": {"sort": "name"},
|
2020-05-27 22:00:04 -07:00
|
|
|
|
"paginated_view": {"size": 25},
|
2018-06-04 09:02:07 -07:00
|
|
|
|
},
|
2018-03-27 09:18:32 -07:00
|
|
|
|
}
|
2019-05-03 22:15:14 -04:00
|
|
|
|
},
|
2018-03-27 09:18:32 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
2017-12-15 04:04:17 -08:00
|
|
|
|
TABLES = (
|
2019-05-03 22:15:14 -04:00
|
|
|
|
"""
|
2017-12-15 04:04:17 -08:00
|
|
|
|
CREATE TABLE simple_primary_key (
|
2025-02-01 21:42:49 -08:00
|
|
|
|
id integer primary key,
|
2017-12-15 04:04:17 -08:00
|
|
|
|
content text
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2018-04-14 07:55:27 -07:00
|
|
|
|
CREATE TABLE primary_key_multiple_columns (
|
|
|
|
|
|
id varchar(30) primary key,
|
|
|
|
|
|
content text,
|
|
|
|
|
|
content2 text
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2018-04-22 13:46:18 -07:00
|
|
|
|
CREATE TABLE primary_key_multiple_columns_explicit_label (
|
|
|
|
|
|
id varchar(30) primary key,
|
|
|
|
|
|
content text,
|
|
|
|
|
|
content2 text
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2017-12-15 04:04:17 -08:00
|
|
|
|
CREATE TABLE compound_primary_key (
|
|
|
|
|
|
pk1 varchar(30),
|
|
|
|
|
|
pk2 varchar(30),
|
|
|
|
|
|
content text,
|
|
|
|
|
|
PRIMARY KEY (pk1, pk2)
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
INSERT INTO compound_primary_key VALUES ('a', 'b', 'c');
|
2022-03-07 07:38:29 -08:00
|
|
|
|
INSERT INTO compound_primary_key VALUES ('a/b', '.c-d', 'c');
|
2026-02-17 18:21:25 +00:00
|
|
|
|
INSERT INTO compound_primary_key VALUES ('d', 'e', 'RENDER_CELL_DEMO');
|
2017-12-15 04:04:17 -08:00
|
|
|
|
|
2018-03-29 22:10:09 -07:00
|
|
|
|
CREATE TABLE compound_three_primary_keys (
|
|
|
|
|
|
pk1 varchar(30),
|
|
|
|
|
|
pk2 varchar(30),
|
|
|
|
|
|
pk3 varchar(30),
|
|
|
|
|
|
content text,
|
|
|
|
|
|
PRIMARY KEY (pk1, pk2, pk3)
|
|
|
|
|
|
);
|
2019-11-09 17:29:36 -08:00
|
|
|
|
CREATE INDEX idx_compound_three_primary_keys_content ON compound_three_primary_keys(content);
|
2018-03-29 22:10:09 -07:00
|
|
|
|
|
2018-04-14 07:55:27 -07:00
|
|
|
|
CREATE TABLE foreign_key_references (
|
|
|
|
|
|
pk varchar(30) primary key,
|
2025-02-01 21:42:49 -08:00
|
|
|
|
foreign_key_with_label integer,
|
|
|
|
|
|
foreign_key_with_blank_label integer,
|
2018-04-14 07:55:27 -07:00
|
|
|
|
foreign_key_with_no_label varchar(30),
|
2020-11-29 11:30:17 -08:00
|
|
|
|
foreign_key_compound_pk1 varchar(30),
|
|
|
|
|
|
foreign_key_compound_pk2 varchar(30),
|
2018-04-14 07:55:27 -07:00
|
|
|
|
FOREIGN KEY (foreign_key_with_label) REFERENCES simple_primary_key(id),
|
2020-11-11 15:37:37 -08:00
|
|
|
|
FOREIGN KEY (foreign_key_with_blank_label) REFERENCES simple_primary_key(id),
|
2018-04-14 07:55:27 -07:00
|
|
|
|
FOREIGN KEY (foreign_key_with_no_label) REFERENCES primary_key_multiple_columns(id)
|
2020-11-29 11:30:17 -08:00
|
|
|
|
FOREIGN KEY (foreign_key_compound_pk1, foreign_key_compound_pk2) REFERENCES compound_primary_key(pk1, pk2)
|
2018-04-14 07:55:27 -07:00
|
|
|
|
);
|
|
|
|
|
|
|
2018-04-08 17:06:10 -07:00
|
|
|
|
CREATE TABLE sortable (
|
|
|
|
|
|
pk1 varchar(30),
|
|
|
|
|
|
pk2 varchar(30),
|
|
|
|
|
|
content text,
|
|
|
|
|
|
sortable integer,
|
|
|
|
|
|
sortable_with_nulls real,
|
|
|
|
|
|
sortable_with_nulls_2 real,
|
2018-04-16 18:41:17 -07:00
|
|
|
|
text text,
|
2018-04-08 17:06:10 -07:00
|
|
|
|
PRIMARY KEY (pk1, pk2)
|
|
|
|
|
|
);
|
2018-03-29 22:10:09 -07:00
|
|
|
|
|
2017-12-15 04:04:17 -08:00
|
|
|
|
CREATE TABLE no_primary_key (
|
|
|
|
|
|
content text,
|
|
|
|
|
|
a text,
|
|
|
|
|
|
b text,
|
|
|
|
|
|
c text
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
CREATE TABLE [123_starts_with_digits] (
|
|
|
|
|
|
content text
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
CREATE VIEW paginated_view AS
|
|
|
|
|
|
SELECT
|
|
|
|
|
|
content,
|
|
|
|
|
|
'- ' || content || ' -' AS content_extra
|
|
|
|
|
|
FROM no_primary_key;
|
|
|
|
|
|
|
|
|
|
|
|
CREATE TABLE "Table With Space In Name" (
|
|
|
|
|
|
pk varchar(30) primary key,
|
|
|
|
|
|
content text
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
CREATE TABLE "table/with/slashes.csv" (
|
|
|
|
|
|
pk varchar(30) primary key,
|
|
|
|
|
|
content text
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
CREATE TABLE "complex_foreign_keys" (
|
|
|
|
|
|
pk varchar(30) primary key,
|
2025-02-01 21:42:49 -08:00
|
|
|
|
f1 integer,
|
|
|
|
|
|
f2 integer,
|
|
|
|
|
|
f3 integer,
|
2017-12-15 04:04:17 -08:00
|
|
|
|
FOREIGN KEY ("f1") REFERENCES [simple_primary_key](id),
|
|
|
|
|
|
FOREIGN KEY ("f2") REFERENCES [simple_primary_key](id),
|
|
|
|
|
|
FOREIGN KEY ("f3") REFERENCES [simple_primary_key](id)
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2018-04-22 10:51:43 -07:00
|
|
|
|
CREATE TABLE "custom_foreign_key_label" (
|
|
|
|
|
|
pk varchar(30) primary key,
|
|
|
|
|
|
foreign_key_with_custom_label text,
|
2018-04-22 13:46:18 -07:00
|
|
|
|
FOREIGN KEY ("foreign_key_with_custom_label") REFERENCES [primary_key_multiple_columns_explicit_label](id)
|
2018-04-22 10:51:43 -07:00
|
|
|
|
);
|
|
|
|
|
|
|
2018-06-21 07:56:28 -07:00
|
|
|
|
CREATE TABLE tags (
|
|
|
|
|
|
tag TEXT PRIMARY KEY
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2018-05-05 19:01:14 -03:00
|
|
|
|
CREATE TABLE searchable (
|
|
|
|
|
|
pk integer primary key,
|
|
|
|
|
|
text1 text,
|
2018-05-05 19:33:08 -03:00
|
|
|
|
text2 text,
|
|
|
|
|
|
[name with . and spaces] text
|
2018-05-05 19:01:14 -03:00
|
|
|
|
);
|
|
|
|
|
|
|
2018-06-21 07:56:28 -07:00
|
|
|
|
CREATE TABLE searchable_tags (
|
|
|
|
|
|
searchable_id integer,
|
|
|
|
|
|
tag text,
|
|
|
|
|
|
PRIMARY KEY (searchable_id, tag),
|
|
|
|
|
|
FOREIGN KEY (searchable_id) REFERENCES searchable(pk),
|
|
|
|
|
|
FOREIGN KEY (tag) REFERENCES tags(tag)
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2018-05-05 19:33:08 -03:00
|
|
|
|
INSERT INTO searchable VALUES (1, 'barry cat', 'terry dog', 'panther');
|
|
|
|
|
|
INSERT INTO searchable VALUES (2, 'terry dog', 'sara weasel', 'puma');
|
2018-05-05 19:01:14 -03:00
|
|
|
|
|
2018-06-21 07:56:28 -07:00
|
|
|
|
INSERT INTO tags VALUES ("canine");
|
|
|
|
|
|
INSERT INTO tags VALUES ("feline");
|
|
|
|
|
|
|
|
|
|
|
|
INSERT INTO searchable_tags (searchable_id, tag) VALUES
|
|
|
|
|
|
(1, "feline"),
|
|
|
|
|
|
(2, "canine")
|
|
|
|
|
|
;
|
|
|
|
|
|
|
2018-05-05 19:01:14 -03:00
|
|
|
|
CREATE VIRTUAL TABLE "searchable_fts"
|
2025-12-12 22:18:35 -08:00
|
|
|
|
USING FTS5 (text1, text2, [name with . and spaces], content="searchable", content_rowid="pk");
|
|
|
|
|
|
INSERT INTO "searchable_fts" (searchable_fts) VALUES ('rebuild');
|
2018-05-05 19:01:14 -03:00
|
|
|
|
|
2018-04-03 06:39:50 -07:00
|
|
|
|
CREATE TABLE [select] (
|
|
|
|
|
|
[group] text,
|
|
|
|
|
|
[having] text,
|
2018-08-04 17:14:56 -07:00
|
|
|
|
[and] text,
|
|
|
|
|
|
[json] text
|
|
|
|
|
|
);
|
|
|
|
|
|
INSERT INTO [select] VALUES ('group', 'having', 'and',
|
|
|
|
|
|
'{"href": "http://example.com/", "label":"Example"}'
|
2018-04-03 06:39:50 -07:00
|
|
|
|
);
|
|
|
|
|
|
|
2018-07-23 20:07:57 -07:00
|
|
|
|
CREATE TABLE infinity (
|
|
|
|
|
|
value REAL
|
|
|
|
|
|
);
|
|
|
|
|
|
INSERT INTO infinity VALUES
|
|
|
|
|
|
(1e999),
|
|
|
|
|
|
(-1e999),
|
|
|
|
|
|
(1.5)
|
|
|
|
|
|
;
|
|
|
|
|
|
|
2018-05-15 10:52:02 -05:00
|
|
|
|
CREATE TABLE facet_cities (
|
|
|
|
|
|
id integer primary key,
|
|
|
|
|
|
name text
|
|
|
|
|
|
);
|
|
|
|
|
|
INSERT INTO facet_cities (id, name) VALUES
|
|
|
|
|
|
(1, 'San Francisco'),
|
|
|
|
|
|
(2, 'Los Angeles'),
|
|
|
|
|
|
(3, 'Detroit'),
|
|
|
|
|
|
(4, 'Memnonia')
|
|
|
|
|
|
;
|
|
|
|
|
|
|
2018-05-12 19:29:06 -03:00
|
|
|
|
CREATE TABLE facetable (
|
|
|
|
|
|
pk integer primary key,
|
2019-05-20 23:09:22 -07:00
|
|
|
|
created text,
|
2018-05-15 10:52:02 -05:00
|
|
|
|
planet_int integer,
|
2018-06-20 21:30:13 -07:00
|
|
|
|
on_earth integer,
|
2018-05-12 19:29:06 -03:00
|
|
|
|
state text,
|
2021-11-29 22:17:27 -08:00
|
|
|
|
_city_id integer,
|
2021-11-13 20:44:54 -08:00
|
|
|
|
_neighborhood text,
|
2019-04-10 08:17:19 -07:00
|
|
|
|
tags text,
|
2019-11-01 12:37:46 -07:00
|
|
|
|
complex_array text,
|
2019-11-21 16:56:55 -08:00
|
|
|
|
distinct_some_null,
|
2022-03-18 18:37:54 -07:00
|
|
|
|
n text,
|
2021-11-29 22:17:27 -08:00
|
|
|
|
FOREIGN KEY ("_city_id") REFERENCES [facet_cities](id)
|
2018-05-12 19:29:06 -03:00
|
|
|
|
);
|
2018-06-20 21:30:13 -07:00
|
|
|
|
INSERT INTO facetable
|
2022-03-18 18:37:54 -07:00
|
|
|
|
(created, planet_int, on_earth, state, _city_id, _neighborhood, tags, complex_array, distinct_some_null, n)
|
2018-06-20 21:30:13 -07:00
|
|
|
|
VALUES
|
2022-03-18 18:37:54 -07:00
|
|
|
|
("2019-01-14 08:00:00", 1, 1, 'CA', 1, 'Mission', '["tag1", "tag2"]', '[{"foo": "bar"}]', 'one', 'n1'),
|
|
|
|
|
|
("2019-01-14 08:00:00", 1, 1, 'CA', 1, 'Dogpatch', '["tag1", "tag3"]', '[]', 'two', 'n2'),
|
|
|
|
|
|
("2019-01-14 08:00:00", 1, 1, 'CA', 1, 'SOMA', '[]', '[]', null, null),
|
|
|
|
|
|
("2019-01-14 08:00:00", 1, 1, 'CA', 1, 'Tenderloin', '[]', '[]', null, null),
|
|
|
|
|
|
("2019-01-15 08:00:00", 1, 1, 'CA', 1, 'Bernal Heights', '[]', '[]', null, null),
|
|
|
|
|
|
("2019-01-15 08:00:00", 1, 1, 'CA', 1, 'Hayes Valley', '[]', '[]', null, null),
|
|
|
|
|
|
("2019-01-15 08:00:00", 1, 1, 'CA', 2, 'Hollywood', '[]', '[]', null, null),
|
|
|
|
|
|
("2019-01-15 08:00:00", 1, 1, 'CA', 2, 'Downtown', '[]', '[]', null, null),
|
|
|
|
|
|
("2019-01-16 08:00:00", 1, 1, 'CA', 2, 'Los Feliz', '[]', '[]', null, null),
|
|
|
|
|
|
("2019-01-16 08:00:00", 1, 1, 'CA', 2, 'Koreatown', '[]', '[]', null, null),
|
|
|
|
|
|
("2019-01-16 08:00:00", 1, 1, 'MI', 3, 'Downtown', '[]', '[]', null, null),
|
|
|
|
|
|
("2019-01-17 08:00:00", 1, 1, 'MI', 3, 'Greektown', '[]', '[]', null, null),
|
|
|
|
|
|
("2019-01-17 08:00:00", 1, 1, 'MI', 3, 'Corktown', '[]', '[]', null, null),
|
|
|
|
|
|
("2019-01-17 08:00:00", 1, 1, 'MI', 3, 'Mexicantown', '[]', '[]', null, null),
|
|
|
|
|
|
("2019-01-17 08:00:00", 2, 0, 'MC', 4, 'Arcadia Planitia', '[]', '[]', null, null)
|
2018-05-12 19:29:06 -03:00
|
|
|
|
;
|
|
|
|
|
|
|
2019-05-03 12:43:59 -04:00
|
|
|
|
CREATE TABLE binary_data (
|
|
|
|
|
|
data BLOB
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2019-05-22 22:44:34 -07:00
|
|
|
|
-- Many 2 Many demo: roadside attractions!
|
|
|
|
|
|
|
|
|
|
|
|
CREATE TABLE roadside_attractions (
|
|
|
|
|
|
pk integer primary key,
|
|
|
|
|
|
name text,
|
|
|
|
|
|
address text,
|
2022-09-06 16:50:43 -07:00
|
|
|
|
url text,
|
2019-05-22 22:44:34 -07:00
|
|
|
|
latitude real,
|
|
|
|
|
|
longitude real
|
|
|
|
|
|
);
|
|
|
|
|
|
INSERT INTO roadside_attractions VALUES (
|
2022-09-06 16:50:43 -07:00
|
|
|
|
1, "The Mystery Spot", "465 Mystery Spot Road, Santa Cruz, CA 95065", "https://www.mysteryspot.com/",
|
2019-05-22 22:44:34 -07:00
|
|
|
|
37.0167, -122.0024
|
|
|
|
|
|
);
|
|
|
|
|
|
INSERT INTO roadside_attractions VALUES (
|
2022-09-06 16:50:43 -07:00
|
|
|
|
2, "Winchester Mystery House", "525 South Winchester Boulevard, San Jose, CA 95128", "https://winchestermysteryhouse.com/",
|
2019-05-22 22:44:34 -07:00
|
|
|
|
37.3184, -121.9511
|
|
|
|
|
|
);
|
|
|
|
|
|
INSERT INTO roadside_attractions VALUES (
|
2022-09-06 16:50:43 -07:00
|
|
|
|
3, "Burlingame Museum of PEZ Memorabilia", "214 California Drive, Burlingame, CA 94010", null,
|
2019-05-22 22:44:34 -07:00
|
|
|
|
37.5793, -122.3442
|
|
|
|
|
|
);
|
|
|
|
|
|
INSERT INTO roadside_attractions VALUES (
|
2022-09-06 16:50:43 -07:00
|
|
|
|
4, "Bigfoot Discovery Museum", "5497 Highway 9, Felton, CA 95018", "https://www.bigfootdiscoveryproject.com/",
|
2019-05-22 22:44:34 -07:00
|
|
|
|
37.0414, -122.0725
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
CREATE TABLE attraction_characteristic (
|
|
|
|
|
|
pk integer primary key,
|
|
|
|
|
|
name text
|
|
|
|
|
|
);
|
|
|
|
|
|
INSERT INTO attraction_characteristic VALUES (
|
|
|
|
|
|
1, "Museum"
|
|
|
|
|
|
);
|
|
|
|
|
|
INSERT INTO attraction_characteristic VALUES (
|
|
|
|
|
|
2, "Paranormal"
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
CREATE TABLE roadside_attraction_characteristics (
|
|
|
|
|
|
attraction_id INTEGER REFERENCES roadside_attractions(pk),
|
|
|
|
|
|
characteristic_id INTEGER REFERENCES attraction_characteristic(pk)
|
|
|
|
|
|
);
|
|
|
|
|
|
INSERT INTO roadside_attraction_characteristics VALUES (
|
|
|
|
|
|
1, 2
|
|
|
|
|
|
);
|
|
|
|
|
|
INSERT INTO roadside_attraction_characteristics VALUES (
|
|
|
|
|
|
2, 2
|
|
|
|
|
|
);
|
|
|
|
|
|
INSERT INTO roadside_attraction_characteristics VALUES (
|
|
|
|
|
|
4, 2
|
|
|
|
|
|
);
|
|
|
|
|
|
INSERT INTO roadside_attraction_characteristics VALUES (
|
|
|
|
|
|
3, 1
|
|
|
|
|
|
);
|
|
|
|
|
|
INSERT INTO roadside_attraction_characteristics VALUES (
|
|
|
|
|
|
4, 1
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2017-12-15 04:04:17 -08:00
|
|
|
|
INSERT INTO simple_primary_key VALUES (1, 'hello');
|
|
|
|
|
|
INSERT INTO simple_primary_key VALUES (2, 'world');
|
|
|
|
|
|
INSERT INTO simple_primary_key VALUES (3, '');
|
2018-08-28 03:03:01 -07:00
|
|
|
|
INSERT INTO simple_primary_key VALUES (4, 'RENDER_CELL_DEMO');
|
2021-08-08 16:04:42 -07:00
|
|
|
|
INSERT INTO simple_primary_key VALUES (5, 'RENDER_CELL_ASYNC');
|
2017-12-15 04:04:17 -08:00
|
|
|
|
|
2018-04-14 07:55:27 -07:00
|
|
|
|
INSERT INTO primary_key_multiple_columns VALUES (1, 'hey', 'world');
|
2018-04-22 13:46:18 -07:00
|
|
|
|
INSERT INTO primary_key_multiple_columns_explicit_label VALUES (1, 'hey', 'world2');
|
2018-04-14 07:55:27 -07:00
|
|
|
|
|
2020-11-29 11:30:17 -08:00
|
|
|
|
INSERT INTO foreign_key_references VALUES (1, 1, 3, 1, 'a', 'b');
|
|
|
|
|
|
INSERT INTO foreign_key_references VALUES (2, null, null, null, null, null);
|
2018-04-14 07:55:27 -07:00
|
|
|
|
|
2017-12-15 04:04:17 -08:00
|
|
|
|
INSERT INTO complex_foreign_keys VALUES (1, 1, 2, 1);
|
2018-04-22 10:51:43 -07:00
|
|
|
|
INSERT INTO custom_foreign_key_label VALUES (1, 1);
|
2017-12-15 04:04:17 -08:00
|
|
|
|
|
|
|
|
|
|
INSERT INTO [table/with/slashes.csv] VALUES (3, 'hey');
|
|
|
|
|
|
|
|
|
|
|
|
CREATE VIEW simple_view AS
|
|
|
|
|
|
SELECT content, upper(content) AS upper_content FROM simple_primary_key;
|
|
|
|
|
|
|
2019-04-11 21:21:17 -07:00
|
|
|
|
CREATE VIEW searchable_view AS
|
|
|
|
|
|
SELECT * from searchable;
|
|
|
|
|
|
|
|
|
|
|
|
CREATE VIEW searchable_view_configured_by_metadata AS
|
|
|
|
|
|
SELECT * from searchable;
|
|
|
|
|
|
|
2019-05-03 22:15:14 -04:00
|
|
|
|
"""
|
2017-12-15 04:04:17 -08:00
|
|
|
|
+ "\n".join(
|
|
|
|
|
|
[
|
|
|
|
|
|
'INSERT INTO no_primary_key VALUES ({i}, "a{i}", "b{i}", "c{i}");'.format(
|
|
|
|
|
|
i=i + 1
|
|
|
|
|
|
)
|
|
|
|
|
|
for i in range(201)
|
2018-03-29 22:10:09 -07:00
|
|
|
|
]
|
|
|
|
|
|
)
|
2026-02-17 18:21:25 +00:00
|
|
|
|
+ '\nINSERT INTO no_primary_key VALUES ("RENDER_CELL_DEMO", "a202", "b202", "c202");\n'
|
2018-03-29 22:10:09 -07:00
|
|
|
|
+ "\n".join(
|
|
|
|
|
|
[
|
2018-03-29 23:26:22 -07:00
|
|
|
|
'INSERT INTO compound_three_primary_keys VALUES ("{a}", "{b}", "{c}", "{content}");'.format(
|
|
|
|
|
|
a=a, b=b, c=c, content=content
|
|
|
|
|
|
)
|
|
|
|
|
|
for a, b, c, content in generate_compound_rows(1001)
|
2018-04-08 17:06:10 -07:00
|
|
|
|
]
|
|
|
|
|
|
)
|
2026-02-17 13:30:24 -08:00
|
|
|
|
+ "\n".join(["""INSERT INTO sortable VALUES (
|
2018-04-08 17:06:10 -07:00
|
|
|
|
"{pk1}", "{pk2}", "{content}", {sortable},
|
2018-04-16 18:41:17 -07:00
|
|
|
|
{sortable_with_nulls}, {sortable_with_nulls_2}, "{text}");
|
2026-02-17 13:30:24 -08:00
|
|
|
|
""".format(**row).replace("None", "null") for row in generate_sortable_rows(201)])
|
2019-05-03 22:15:14 -04:00
|
|
|
|
)
|
2019-05-03 12:43:59 -04:00
|
|
|
|
TABLE_PARAMETERIZED_SQL = [
|
2020-08-16 11:24:39 -07:00
|
|
|
|
("insert into binary_data (data) values (?);", [b"\x15\x1c\x02\xc7\xad\x05\xfe"]),
|
|
|
|
|
|
("insert into binary_data (data) values (?);", [b"\x15\x1c\x03\xc7\xad\x05\xfe"]),
|
2020-10-28 21:05:40 -07:00
|
|
|
|
("insert into binary_data (data) values (null);", []),
|
2019-05-03 12:43:59 -04:00
|
|
|
|
]
|
2019-05-03 22:15:14 -04:00
|
|
|
|
|
2019-05-14 08:46:57 -07:00
|
|
|
|
EXTRA_DATABASE_SQL = """
|
|
|
|
|
|
CREATE TABLE searchable (
|
|
|
|
|
|
pk integer primary key,
|
|
|
|
|
|
text1 text,
|
|
|
|
|
|
text2 text
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2019-05-15 17:28:07 -07:00
|
|
|
|
CREATE VIEW searchable_view AS SELECT * FROM searchable;
|
|
|
|
|
|
|
2019-05-14 08:46:57 -07:00
|
|
|
|
INSERT INTO searchable VALUES (1, 'barry cat', 'terry dog');
|
|
|
|
|
|
INSERT INTO searchable VALUES (2, 'terry dog', 'sara weasel');
|
|
|
|
|
|
|
|
|
|
|
|
CREATE VIRTUAL TABLE "searchable_fts"
|
|
|
|
|
|
USING FTS3 (text1, text2, content="searchable");
|
|
|
|
|
|
INSERT INTO "searchable_fts" (rowid, text1, text2)
|
|
|
|
|
|
SELECT rowid, text1, text2 FROM searchable;
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
2020-06-06 22:30:36 -07:00
|
|
|
|
|
2020-06-07 13:03:08 -07:00
|
|
|
|
def assert_permissions_checked(datasette, actions):
|
2020-06-08 11:59:11 -07:00
|
|
|
|
# actions is a list of "action" or (action, resource) tuples
|
2020-06-07 13:03:08 -07:00
|
|
|
|
for action in actions:
|
|
|
|
|
|
if isinstance(action, str):
|
2020-06-08 11:59:11 -07:00
|
|
|
|
resource = None
|
2020-06-07 13:03:08 -07:00
|
|
|
|
else:
|
2020-06-08 11:59:11 -07:00
|
|
|
|
action, resource = action
|
2025-10-25 10:03:41 -07:00
|
|
|
|
|
|
|
|
|
|
# Convert PermissionCheck dataclass to old resource format for comparison
|
|
|
|
|
|
def check_matches(pc, action, resource):
|
|
|
|
|
|
if pc.action != action:
|
|
|
|
|
|
return False
|
|
|
|
|
|
# Convert parent/child to old resource format
|
|
|
|
|
|
if pc.parent and pc.child:
|
|
|
|
|
|
pc_resource = (pc.parent, pc.child)
|
|
|
|
|
|
elif pc.parent:
|
|
|
|
|
|
pc_resource = pc.parent
|
|
|
|
|
|
else:
|
|
|
|
|
|
pc_resource = None
|
|
|
|
|
|
return pc_resource == resource
|
|
|
|
|
|
|
2020-06-07 13:03:08 -07:00
|
|
|
|
assert [
|
|
|
|
|
|
pc
|
|
|
|
|
|
for pc in datasette._permission_checks
|
2025-10-25 10:03:41 -07:00
|
|
|
|
if check_matches(pc, action, resource)
|
2020-06-08 11:59:11 -07:00
|
|
|
|
], """Missing expected permission check: action={}, resource={}
|
2020-06-07 13:03:08 -07:00
|
|
|
|
Permission checks seen: {}
|
|
|
|
|
|
""".format(
|
2020-09-02 15:24:55 -07:00
|
|
|
|
action,
|
|
|
|
|
|
resource,
|
2025-10-25 10:03:41 -07:00
|
|
|
|
json.dumps(
|
|
|
|
|
|
[
|
|
|
|
|
|
{
|
|
|
|
|
|
"action": pc.action,
|
|
|
|
|
|
"parent": pc.parent,
|
|
|
|
|
|
"child": pc.child,
|
|
|
|
|
|
"result": pc.result,
|
|
|
|
|
|
}
|
|
|
|
|
|
for pc in datasette._permission_checks
|
|
|
|
|
|
],
|
|
|
|
|
|
indent=4,
|
|
|
|
|
|
),
|
2020-06-07 13:03:08 -07:00
|
|
|
|
)
|
2020-06-09 12:57:54 -07:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@click.command()
|
|
|
|
|
|
@click.argument(
|
|
|
|
|
|
"db_filename",
|
|
|
|
|
|
default="fixtures.db",
|
|
|
|
|
|
type=click.Path(file_okay=True, dir_okay=False),
|
|
|
|
|
|
)
|
2023-09-13 14:06:25 -07:00
|
|
|
|
@click.argument("config", required=False)
|
2023-09-21 13:22:40 -07:00
|
|
|
|
@click.argument("metadata", required=False)
|
2020-06-09 12:57:54 -07:00
|
|
|
|
@click.argument(
|
|
|
|
|
|
"plugins_path", type=click.Path(file_okay=False, dir_okay=True), required=False
|
|
|
|
|
|
)
|
|
|
|
|
|
@click.option(
|
|
|
|
|
|
"--recreate",
|
|
|
|
|
|
is_flag=True,
|
|
|
|
|
|
default=False,
|
|
|
|
|
|
help="Delete and recreate database if it exists",
|
|
|
|
|
|
)
|
2021-02-18 14:09:12 -08:00
|
|
|
|
@click.option(
|
|
|
|
|
|
"--extra-db-filename",
|
|
|
|
|
|
type=click.Path(file_okay=True, dir_okay=False),
|
|
|
|
|
|
help="Write out second test DB to this file",
|
|
|
|
|
|
)
|
2023-09-13 14:06:25 -07:00
|
|
|
|
def cli(db_filename, config, metadata, plugins_path, recreate, extra_db_filename):
|
2020-12-23 18:04:32 +01:00
|
|
|
|
"""Write out the fixtures database used by Datasette's test suite"""
|
2020-06-09 12:57:54 -07:00
|
|
|
|
if metadata and not metadata.endswith(".json"):
|
|
|
|
|
|
raise click.ClickException("Metadata should end with .json")
|
|
|
|
|
|
if not db_filename.endswith(".db"):
|
|
|
|
|
|
raise click.ClickException("Database file should end with .db")
|
|
|
|
|
|
if pathlib.Path(db_filename).exists():
|
|
|
|
|
|
if not recreate:
|
|
|
|
|
|
raise click.ClickException(
|
2020-11-15 15:24:22 -08:00
|
|
|
|
f"{db_filename} already exists, use --recreate to reset it"
|
2020-06-09 12:57:54 -07:00
|
|
|
|
)
|
|
|
|
|
|
else:
|
|
|
|
|
|
pathlib.Path(db_filename).unlink()
|
|
|
|
|
|
conn = sqlite3.connect(db_filename)
|
|
|
|
|
|
conn.executescript(TABLES)
|
|
|
|
|
|
for sql, params in TABLE_PARAMETERIZED_SQL:
|
|
|
|
|
|
with conn:
|
|
|
|
|
|
conn.execute(sql, params)
|
2020-11-15 15:24:22 -08:00
|
|
|
|
print(f"Test tables written to {db_filename}")
|
2020-06-09 12:57:54 -07:00
|
|
|
|
if metadata:
|
2021-03-11 17:15:49 +01:00
|
|
|
|
with open(metadata, "w") as fp:
|
|
|
|
|
|
fp.write(json.dumps(METADATA, indent=4))
|
2020-11-15 15:24:22 -08:00
|
|
|
|
print(f"- metadata written to {metadata}")
|
2023-09-13 14:06:25 -07:00
|
|
|
|
if config:
|
|
|
|
|
|
with open(config, "w") as fp:
|
|
|
|
|
|
fp.write(json.dumps(CONFIG, indent=4))
|
|
|
|
|
|
print(f"- config written to {config}")
|
2020-06-09 12:57:54 -07:00
|
|
|
|
if plugins_path:
|
|
|
|
|
|
path = pathlib.Path(plugins_path)
|
|
|
|
|
|
if not path.exists():
|
|
|
|
|
|
path.mkdir()
|
|
|
|
|
|
test_plugins = pathlib.Path(__file__).parent / "plugins"
|
|
|
|
|
|
for filepath in test_plugins.glob("*.py"):
|
|
|
|
|
|
newpath = path / filepath.name
|
2021-03-11 17:15:49 +01:00
|
|
|
|
newpath.write_text(filepath.read_text())
|
2020-11-15 15:24:22 -08:00
|
|
|
|
print(f" Wrote plugin: {newpath}")
|
2021-02-18 14:09:12 -08:00
|
|
|
|
if extra_db_filename:
|
|
|
|
|
|
if pathlib.Path(extra_db_filename).exists():
|
|
|
|
|
|
if not recreate:
|
|
|
|
|
|
raise click.ClickException(
|
|
|
|
|
|
f"{extra_db_filename} already exists, use --recreate to reset it"
|
|
|
|
|
|
)
|
|
|
|
|
|
else:
|
|
|
|
|
|
pathlib.Path(extra_db_filename).unlink()
|
|
|
|
|
|
conn = sqlite3.connect(extra_db_filename)
|
|
|
|
|
|
conn.executescript(EXTRA_DATABASE_SQL)
|
|
|
|
|
|
print(f"Test tables written to {extra_db_filename}")
|
2020-06-09 12:57:54 -07:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
|
cli()
|