# # Copyright (c) 2009-2023 Tom Keffer # # 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()