SpatiaLite with pyenv on Mac


GeoDjangoを使うためのSpatial database。
本番用としてPostGISを使うときは問題なかったが、ローカルテストやCI用としてSQLite上で動かしたく、SpatiaLiteを使おうとするとmigrateでつまづく。
ドキュメント通りはいかなかったものを解決した話

Environment

  • macOS Sierra 10.12.6
  • pyenv 1.1.4
  • Python 3.6.2
  • Django 1.11.6

TL;DR

$ LDFLAGS="-L/usr/local/opt/sqlite/lib -L/usr/local/opt/zlib/lib" CPPFLAGS="-I/usr/local/opt/sqlite/include -I/usr/local/opt/zlib/include" PYTHON_CONFIGURE_OPTS="--enable-loadable-sqlite-extensions" pyenv install 3.6.2

原因

  • Pythonインストール時のsqlite3パッケージがMacのbuilt-inのSQLiteとリンクされていた
  • pyenvでインストールしたPythonが--enable-loadable-sqlite-extensionsじゃなかった

事の顛末

Installing SpatiaLite

$ brew update
$ brew install spatialite-tools
$ brew install gdal

GEOS/PROJやらはGDALが依存してるのでついてくる

settings.py
SPATIALITE_LIBRARY_PATH = '/usr/local/lib/mod_spatialite.dylib'

これだけだとそもそもextensionがロードできないのでエラーになる。
起こる場所はここ
https://github.com/django/django/blob/1.11.6/django/contrib/gis/db/backends/spatialite/base.py#L52

    def get_new_connection(self, conn_params):
        conn = super(DatabaseWrapper, self).get_new_connection(conn_params)
        # Enabling extension loading on the SQLite connection.
        try:
            conn.enable_load_extension(True)
        except AttributeError:
            raise ImproperlyConfigured(
                'The pysqlite library does not support C extension loading. '
                'Both SQLite and pysqlite must be configured to allow '
                'the loading of extensions to use SpatiaLite.')
        # Loading the SpatiaLite library extension on the connection, and returning
        # the created cursor.
        cur = conn.cursor(factory=SQLiteCursorWrapper)
        try:
            cur.execute("SELECT load_extension(%s)", (self.spatialite_lib,))
        except Exception as msg:
            new_msg = (
                'Unable to load the SpatiaLite library extension '
                '"%s" because: %s') % (self.spatialite_lib, msg)
            six.reraise(ImproperlyConfigured, ImproperlyConfigured(new_msg), sys.exc_info()[2])
        cur.close()
        return conn

最初はpysqliteに惑わされてそちら辺り調べたが、Python3ではそもそもpysqliteじゃなくsqlite3を使うので全く見当違いだった。

SQLite extension

直接sqliteからextensionをロードしてみよう

SQLiteConsole
sqlite> SELECT load_extension('/usr/local/lib/mod_spatialite.dylib')
Error: no such function: load_extension
$ sqlite3 --version
3.16.0
$ which sqlite3
/usr/bin/sqlite3

あー

とりまsqlite3パッケージにのコネクションにenable_load_extensionがないのでsqlite3のソースをたどったら
sqlite3sqlite3.dbapi2_sqlite3

IPython
In [1]: import sqlite3
In [2]: sqlite3.sqlite_version
Out[2]: '3.16.0'  # mac built-in

In [3]: import _sqlite3
In [4]: _sqlite3.__file__
Out[4]: '/PATH/TO/.venv/lib/python3.6/lib-dynload/_sqlite3.cpython-36m-darwin.so'

あー

HomebrewでインストールされたSQLite3だと問題なくload_extensionできた。

$ /usr/local/opt/sqlite3/bin/sqlite3 --version
3.22.0

ソースレベルじゃどうしようもないんで、djangojaの助言通りPythonのインストールからやり直し

pyenv with configurations

ググったら出たやつ1

https://trac.macports.org/ticket/49317
MacPortsは使ってないけどまあ--enable-loadable-sqlite-extensionsこれをつけないといけないらしい

ググったら出たやつ2

https://github.com/pyenv/pyenv/issues/333
sqlite3パッケージをHomebrewのやつとリンクさせるには以下で行けるらしい

$ LDFLAGS="-L/usr/local/opt/sqlite/lib -L/usr/local/opt/zlib/lib" CPPFLAGS="-I/usr/local/opt/sqlite/include -I/usr/local/opt/zlib/include" pyenv install 3.6.2

実際やってみると

InteractiveConsole
>>> import sqlite3
>>> sqlite3.sqlite_version
'3.22.0'  # やったか!?
>>> conn = sqlite3.connect('tmp.db')
>>> conn.enable_load_extension(True)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'sqlite3.Connection' object has no attribute 'enable_load_extension'
# やってない

ググったら出たやつ3

https://github.com/pyenv/pyenv/issues/86
Pythonにオプションを付けるには以下で行けるらしい

$ PYTHON_CONFIGURE_OPTS="<configurations>" pyenv install 3.6.2

これだけでやったら

WARNING: The Python sqlite3 extension was not compiled. Missing the SQLite3 lib?
Installed Python-3.6.2 to /PATH/TO/.pyenv/versions/3.6.2

あれ・・・

InteractiveConsole
>>> import sqlite3
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/PATH/TO/.pyenv/versions/3.6.2/lib/python3.6/sqlite3/__init__.py", line 23, in <module>
    from sqlite3.dbapi2 import *
  File "/PATH/TO/.pyenv/versions/3.6.2/lib/python3.6/sqlite3/dbapi2.py", line 27, in <module>
    from _sqlite3 import *
ModuleNotFoundError: No module named '_sqlite3'

多分built-inのSQLite3にlibsqlite3-devだったかなんだかが抜けてるかなんとか…

全部合わせて

$ LDFLAGS="-L/usr/local/opt/sqlite/lib -L/usr/local/opt/zlib/lib" CPPFLAGS="-I/usr/local/opt/sqlite/include -I/usr/local/opt/zlib/include" PYTHON_CONFIGURE_OPTS="--enable-loadable-sqlite-extensions" pyenv install 3.6.2
InteractiveConsole
>>> import sqlite3
>>> conn = sqlite3.connect('tmp.db')
>>> conn.enable_load_extension(True)
>>> conn.load_extension('/usr/local/lib/mod_spatialite.dylib')

問題なし
GeoDjangoのmigrate/test/runserverともに問題なかった

終わりに

  • Homebrewで直接インストールしたPython3のときはわからん。pyenv使いましょう。
  • Pythonパッケージのsqlite3とDBのSQLite3をはっきり区別して考えよう。
  • 本番ではPostGISを使いましょう。

References