Files
weewx/bin/weedb/sqlite.py
Tom Keffer b1677ee85f Revert part of commit 35c7b61b.
Do not change the API for weedb.sqlite.
2023-01-29 16:17:33 -08:00

298 lines
11 KiB
Python

#
# Copyright (c) 2009-2023 Tom Keffer <tkeffer@gmail.com>
#
# See the file LICENSE.txt for your full rights.
#
"""weedb driver for sqlite"""
import os.path
# Import sqlite3. If it does not support the 'with' statement, then
# import pysqlite2, which might...
import sqlite3
if not hasattr(sqlite3.Connection, "__exit__"): # @UndefinedVariable
del sqlite3
from pysqlite2 import dbapi2 as sqlite3 # @Reimport @UnresolvedImport
# Test to see whether this version of SQLite has math functions. An explicit test is required
# (rather than just check version numbers) because the SQLite library may or may not have been
# compiled with the DSQLITE_ENABLE_MATH_FUNCTIONS option.
try:
with sqlite3.connect(":memory:") as conn:
conn.execute("SELECT RADIANS(0.0), SIN(0.0), COS(0.0);")
except sqlite3.OperationalError:
has_math = False
else:
has_math = True
import weedb
from weeutil.weeutil import to_int, to_bool
def guard(fn):
"""Decorator function that converts sqlite exceptions into weedb exceptions."""
def guarded_fn(*args, **kwargs):
try:
return fn(*args, **kwargs)
except sqlite3.IntegrityError as e:
raise weedb.IntegrityError(e)
except sqlite3.OperationalError as e:
msg = str(e).lower()
if msg.startswith("unable to open"):
raise weedb.PermissionError(e)
elif msg.startswith("no such table"):
raise weedb.NoTableError(e)
elif msg.endswith("already exists"):
raise weedb.TableExistsError(e)
elif msg.startswith("no such column"):
raise weedb.NoColumnError(e)
else:
raise weedb.OperationalError(e)
except sqlite3.ProgrammingError as e:
raise weedb.ProgrammingError(e)
return guarded_fn
def connect(database_name='', SQLITE_ROOT='', driver='', **argv): # @UnusedVariable
"""Factory function, to keep things compatible with DBAPI. """
return Connection(database_name=database_name, SQLITE_ROOT=SQLITE_ROOT, **argv)
@guard
def create(database_name='', SQLITE_ROOT='', driver='', **argv): # @UnusedVariable
"""Create the database specified by the db_dict. If it already exists,
an exception of type DatabaseExistsError will be thrown."""
file_path = _get_filepath(SQLITE_ROOT, database_name, **argv)
# Check whether the database file exists:
if os.path.exists(file_path):
raise weedb.DatabaseExistsError("Database %s already exists" % (file_path,))
else:
if file_path != ':memory:':
# If it doesn't exist, create the parent directories
fileDirectory = os.path.dirname(file_path)
if not os.path.exists(fileDirectory):
try:
os.makedirs(fileDirectory)
except OSError:
raise weedb.PermissionError("No permission to create %s" % fileDirectory)
timeout = to_int(argv.get('timeout', 5))
isolation_level = argv.get('isolation_level')
# Open, then immediately close the database.
connection = sqlite3.connect(file_path, timeout=timeout, isolation_level=isolation_level)
connection.close()
def drop(database_name='', SQLITE_ROOT='', driver='', **argv): # @UnusedVariable
file_path = _get_filepath(SQLITE_ROOT, database_name, **argv)
try:
os.remove(file_path)
except OSError as e:
errno = getattr(e, 'errno', 2)
if errno == 13:
raise weedb.PermissionError("No permission to drop database %s" % file_path)
else:
raise weedb.NoDatabaseError("Attempt to drop non-existent database %s" % file_path)
def _get_filepath(SQLITE_ROOT, database_name, **argv):
"""Utility function to calculate the path to the sqlite database file."""
if database_name == ':memory:':
return database_name
# For backwards compatibility, allow the keyword 'root', if 'SQLITE_ROOT' is
# not defined:
root_dir = SQLITE_ROOT or argv.get('root', '')
return os.path.join(root_dir, database_name)
class Connection(weedb.Connection):
"""A wrapper around a sqlite3 connection object."""
@guard
def __init__(self, database_name='', SQLITE_ROOT='', pragmas=None, **argv):
"""Initialize an instance of Connection.
Args:
database_name: The name of the Sqlite database. This is generally the file name
SQLITE_ROOT: The path to the directory holding the database. Joining "SQLITE_ROOT" with
"database_name" results in the full path to the sqlite file.
pragmas: Any pragma statements, in the form of a dictionary.
timeout: The amount of time, in seconds, to wait for a lock to be released.
Optional. Default is 5.
isolation_level(str): The type of isolation level to use. One of None,
DEFERRED, IMMEDIATE, or EXCLUSIVE. Default is None (autocommit mode).
Raises:
NoDatabaseError: If the database file does not exist.
"""
self.file_path = _get_filepath(SQLITE_ROOT, database_name, **argv)
if self.file_path != ':memory:' and not os.path.exists(self.file_path):
raise weedb.NoDatabaseError("Attempt to open a non-existent database %s"
% self.file_path)
timeout = to_int(argv.get('timeout', 5))
isolation_level = argv.get('isolation_level')
connection = sqlite3.connect(self.file_path, timeout=timeout,
isolation_level=isolation_level)
if pragmas:
for pragma in pragmas:
connection.execute("PRAGMA %s=%s;" % (pragma, pragmas[pragma]))
weedb.Connection.__init__(self, connection, database_name, 'sqlite')
@guard
def cursor(self):
"""Return a cursor object."""
return self.connection.cursor(Cursor)
@guard
def execute(self, sql_string, sql_tuple=()):
"""Execute a sql statement. This specialized version takes advantage
of sqlite's ability to do an execute without a cursor."""
with self.connection:
self.connection.execute(sql_string, sql_tuple)
@guard
def tables(self):
"""Returns a list of tables in the database."""
table_list = list()
for row in self.connection.execute("SELECT tbl_name FROM sqlite_master "
"WHERE type='table';"):
# Extract the table name. Sqlite returns unicode, so always
# convert to a regular string:
table_list.append(str(row[0]))
return table_list
@guard
def genSchemaOf(self, table):
"""Return a summary of the schema of the specified table.
If the table does not exist, an exception of type weedb.OperationalError is raised."""
for row in self.connection.execute("""PRAGMA table_info(%s);""" % table):
if row[2].upper().startswith('CHAR'):
coltype = 'STR'
else:
coltype = str(row[2]).upper()
yield (row[0], str(row[1]), coltype, not to_bool(row[3]), row[4], to_bool(row[5]))
def columnsOf(self, table):
"""Return a list of columns in the specified table. If the table does not exist,
None is returned."""
column_list = [row[1] for row in self.genSchemaOf(table)]
# If there are no columns (which means the table did not exist) raise an exceptional
if not column_list:
raise weedb.ProgrammingError("No such table %s" % table)
return column_list
@guard
def get_variable(self, var_name):
cursor = self.connection.cursor()
try:
cursor.execute("PRAGMA %s;" % var_name)
row = cursor.fetchone()
return None if row is None else (var_name, row[0])
finally:
cursor.close()
@property
def has_math(self):
global has_math
return has_math
@guard
def begin(self):
self.connection.execute("BEGIN TRANSACTION")
@guard
def commit(self):
self.connection.commit()
@guard
def rollback(self):
self.connection.rollback()
@guard
def close(self):
self.connection.close()
class Cursor(sqlite3.Cursor):
"""A wrapper around the sqlite cursor object"""
# The sqlite3 cursor object is very full featured. We need only turn
# the sqlite exceptions into weedb exceptions.
@guard
def execute(self, *args, **kwargs):
return sqlite3.Cursor.execute(self, *args, **kwargs)
@guard
def fetchone(self):
return sqlite3.Cursor.fetchone(self)
@guard
def fetchall(self):
return sqlite3.Cursor.fetchall(self)
@guard
def fetchmany(self, size=None):
if size is None: size = self.arraysize
return sqlite3.Cursor.fetchmany(self, size)
def drop_columns(self, table, column_names):
"""Drop the set of 'column_names' from table 'table'.
table: The name of the table from which the column(s) are to be dropped.
column_names: A set (or list) of column names to be dropped. It is not an error to try to
drop a non-existent column.
"""
existing_column_set = set()
create_list = []
insert_list = []
self.execute("""PRAGMA table_info(%s);""" % table)
for row in self.fetchall():
# Unpack the row
row_no, obs_name, obs_type, no_null, default, pk = row
existing_column_set.add(obs_name)
# Search through the target columns.
if obs_name in column_names:
continue
no_null_str = " NOT NULL" if no_null else ""
pk_str = " UNIQUE PRIMARY KEY" if pk else ""
default_str = " DEFAULT %s" % default if default is not None else ""
create_list.append("`%s` %s%s%s%s" % (obs_name, obs_type, no_null_str,
pk_str, default_str))
insert_list.append(obs_name)
for column in column_names:
if column not in existing_column_set:
raise weedb.NoColumnError("Cannot DROP '%s'; column does not exist." % column)
create_str = ", ".join(create_list)
insert_str = ", ".join(insert_list)
self.execute("CREATE TEMPORARY TABLE %s_temp (%s);" % (table, create_str))
self.execute("INSERT INTO %s_temp SELECT %s FROM %s;" % (table, insert_str, table))
self.execute("DROP TABLE %s;" % table)
self.execute("CREATE TABLE %s (%s);" % (table, create_str))
self.execute("INSERT INTO %s SELECT %s FROM %s_temp;" % (table, insert_str, table))
self.execute("DROP TABLE %s_temp;" % table)
def __enter__(self):
return self
def __exit__(self, etyp, einst, etb): # @UnusedVariable
# It is not an error to close a sqlite3 cursor multiple times,
# so there's no reason to guard it with a "try" clause:
self.close()