From da0b3ce2b76227f1cd0cfdd3df361712c17ddcd0 Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Sat, 11 May 2019 15:55:30 -0700 Subject: [PATCH] New run_sanity_checks mechanism, for SpatiLite Moved VirtualSpatialIndex check into a new mechanism that should allow us to add further sanity checks in the future. To test this I've had to commit a binary sample SpatiaLite database to the repository. I included a build script for creating that database. Closes #466 --- .gitignore | 5 +-- datasette/app.py | 56 +++++++++++++++---------- tests/build_small_spatialite_db.py | 23 ++++++++++ tests/spatialite.db | Bin 0 -> 221184 bytes tests/{test_inspect.py => test_cli.py} | 10 +++++ 5 files changed, 68 insertions(+), 26 deletions(-) create mode 100644 tests/build_small_spatialite_db.py create mode 100644 tests/spatialite.db rename tests/{test_inspect.py => test_cli.py} (75%) diff --git a/.gitignore b/.gitignore index d8e19d02..47418755 100644 --- a/.gitignore +++ b/.gitignore @@ -9,9 +9,8 @@ scratchpad Pipfile Pipfile.lock -# SQLite databases -*.db -*.sqlite +fixtures.db +*test.db # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/datasette/app.py b/datasette/app.py index f6ee38f1..707f3b52 100644 --- a/datasette/app.py +++ b/datasette/app.py @@ -344,6 +344,28 @@ class Datasette: except ValueError: # Plugin already registered pass + # Run the sanity checks + asyncio.get_event_loop().run_until_complete(self.run_sanity_checks()) + + async def run_sanity_checks(self): + # Only one check right now, for Spatialite + for database_name, database in self.databases.items(): + # Run pragma_info on every table + for table in await database.table_names(): + try: + await self.execute( + database_name, + "PRAGMA table_info({});".format(escape_sqlite(table)), + ) + except sqlite3.OperationalError as e: + if e.args[0] == "no such module: VirtualSpatialIndex": + raise click.UsageError( + "It looks like you're trying to load a SpatiaLite" + " database without first loading the SpatiaLite module." + "\n\nRead more: https://datasette.readthedocs.io/en/latest/spatialite.html" + ) + else: + raise def config(self, key): return self._config.get(key, None) @@ -530,29 +552,17 @@ class Datasette: name = path.stem if name in self._inspect: raise Exception("Multiple files with same stem %s" % name) - try: - with sqlite3.connect( - "file:{}?mode=ro".format(path), uri=True - ) as conn: - self.prepare_connection(conn) - self._inspect[name] = { - "hash": inspect_hash(path), - "file": str(path), - "size": path.stat().st_size, - "views": inspect_views(conn), - "tables": inspect_tables( - conn, (self.metadata("databases") or {}).get(name, {}) - ), - } - except sqlite3.OperationalError as e: - if e.args[0] == "no such module: VirtualSpatialIndex": - raise click.UsageError( - "It looks like you're trying to load a SpatiaLite" - " database without first loading the SpatiaLite module." - "\n\nRead more: https://datasette.readthedocs.io/en/latest/spatialite.html" - ) - else: - raise + with sqlite3.connect("file:{}?mode=ro".format(path), uri=True) as conn: + self.prepare_connection(conn) + self._inspect[name] = { + "hash": inspect_hash(path), + "file": str(path), + "size": path.stat().st_size, + "views": inspect_views(conn), + "tables": inspect_tables( + conn, (self.metadata("databases") or {}).get(name, {}) + ), + } return self._inspect def register_custom_units(self): diff --git a/tests/build_small_spatialite_db.py b/tests/build_small_spatialite_db.py new file mode 100644 index 00000000..c6b0593b --- /dev/null +++ b/tests/build_small_spatialite_db.py @@ -0,0 +1,23 @@ +import sqlite3 + +# This script generates the spatialite.db file in our tests directory. + + +def generate_it(filename): + conn = sqlite3.connect(filename) + # Lead the spatialite extension: + conn.enable_load_extension(True) + conn.load_extension("/usr/local/lib/mod_spatialite.dylib") + conn.execute("select InitSpatialMetadata(1)") + conn.executescript("create table museums (name text)") + conn.execute("SELECT AddGeometryColumn('museums', 'point_geom', 4326, 'POINT', 2);") + # At this point it is around 5MB - we can shrink it dramatically by doing thisO + conn.execute("delete from spatial_ref_sys") + conn.execute("delete from spatial_ref_sys_aux") + conn.commit() + conn.execute("vacuum") + conn.close() + + +if __name__ == "__main__": + generate_it("spatialite.db") diff --git a/tests/spatialite.db b/tests/spatialite.db new file mode 100644 index 0000000000000000000000000000000000000000..c88d5b1fabfab1e343f260ca5d89dbc29345b989 GIT binary patch literal 221184 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