from dataclasses import dataclass from typing import Optional from .exit_codes import SQL_ERROR import argparse import gettext import hashlib import pathlib import sqlite3 import sys _ = gettext.gettext PRGR_NAME = 'sqlite-to-cpp' def list_tables(cx): """Return a list of tables in database.""" cx.execute(""" SELECT name FROM sqlite_schema WHERE name NOT LIKE 'sqlite_%' AND type IN ('table', 'view') UNION ALL SELECT name FROM temp.sqlite_schema WHERE name NOT LIKE 'sqlite_%' AND type IN ('table', 'view') """) return [name for (name,) in cx.fetchall()] @dataclass class TableInfo: """Information about one column of one SQLite table.""" name: str type: str nullable: bool default: str def table_info(cx, table_name): """ Return information about each column in table. Returns a """ cx.execute(""" SELECT [name], [type], [notnull], [dflt_value] FROM pragma_table_info(?) ORDER BY cid ASC """, (table_name,)) return [TableInfo(name=name, type=type, nullable=notnull == 0, default=dflt_value) for (name, type, notnull, dflt_value) in cx.fetchall()] def trans_name(name): """Transform the value of a column to something better matched with C++.""" return ''.join(n.title() for n in name.split('_')) def trans_table(name): """Transform the value of a table to something better matched with C++.""" return trans_name(name) def indent(s): """Prepend for spaces to string.""" return ' '*4 + s def reflow(block): """ Take a multi-line string, remove indention, and return it as a list. The first line shoud be empty. The indentation to remove is taken from the second line. """ lines = block.split('\n') if not lines: return lines prefix_len = 0 for c in lines[1]: if not c.isspace(): break prefix_len += 1 return (line[prefix_len:] for line in lines) def enum_declaration(table, info): """Return a C++ enum declaration.""" return map(indent, [ f'enum {trans_table(table)} {{', *(indent(f'{trans_name(record.name)},') for record in info), f'}}; /* enum {trans_table(table)} */']) def enum_names_declaration(table, info): """Return a C++ string arroy of the names of an enum.""" table = trans_table(table) return map(indent, [ "const char* names[] = { ", *(indent(f'"{table}::{trans_name(record.name)}",') for record in info), "}; /* char* names[] */"]) def enum_out_operator_declaration(table): """Return a C++ operator<< definition for printing an enum.""" type = trans_table(table) return map(indent, reflow(f""" std::ostream& operator<< (std::ostream& out, const {type}& it) {{ out << names[it]; return out; }} /* operator<< */ """)) def run_from_commandline(): """Entry point of program.""" gettext.bindtextdomain(PRGR_NAME, 'translation') gettext.textdomain('translation') parser = argparse.ArgumentParser( prog=PRGR_NAME, description=_('Generate C++ enums from an SQLite database.')) parser.add_argument( '-o', '--output', type=pathlib.Path, help=_('Target output file')) parser.add_argument( 'schema', help=_('File containing SQLite schema.'), type=argparse.FileType('r')) parser.add_argument( '--header-guard', help=_('Contents of header guard. Defaults to a hash of the file')) parser.add_argument( '--namespace', default='DB', action='store', help=_('Name of top-level C++ namespace.')) parser.add_argument( '--no-namespace', action='store_true', help=_('Disable top level C++ namespace.')) parser.add_argument( '--warning-directives', action='store_true', help=_('Include #warning directives.')) args = parser.parse_args() body = sqlite_to_cpp( schema=args.schema.read(), warning_directives=args.warning_directives, no_namespace=args.no_namespace, namespace=args.namespace) print(body, file=args.output) sys.exit(0) def sqlite_to_cpp(*, schema: str, warning_directives: bool = False, no_namespace: bool = False, namespace: Optional[str] = None, header_guard: Optional[str] = None): """ Generate C++ code from an SQLite schema. [parameters] schema - String containing SQL statements for creating the database. warning_directives - Should '#warning' directives be inserted into the output no_namespace - Omit the top-level namespace namespace - Change name of top level namespace header_guard - Explicit value to use for header guard. If none is specified then one is generated. """ return_value = 0 db = sqlite3.connect(':memory:') cx = db.cursor() cx.executescript(schema) lines = [] for table in list_tables(cx): try: lines.append(f'namespace {trans_table(table)} {{') info = table_info(cx, table) lines.extend(enum_declaration(table, info)) # lines.append('') # lines.extend(enum_names_declaration(table, info)) # lines.extend(enum_out_operator_declaration(table)) except sqlite3.OperationalError as e: # TODO emit error somewhere more visibile return_value = SQL_ERROR # TODO sanitize e when included in the output lines.append(f"/*\n{e}\n*/") if warning_directives: lines.append(f'#warning "{PRGR_NAME}: {e}"') lines.append('') lines.extend([ f'}}; /* namespace {trans_table(table)} */', '']) body = '\n'.join(lines) if not no_namespace: body = f'namespace {namespace} {{\n{body}\n}}\n' guard = header_guard \ or ('SHA256' + hashlib.sha256(body.encode('UTF-8')).hexdigest()) body = f""" #ifndef {guard} #define {guard} {body} #endif /* {guard} */ """ return body.strip()