From 98268d3fc48e95cdff7bfbfb288c5ffdc34bf6f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= <hugo@lysator.liu.se> Date: Sun, 23 Apr 2023 14:55:11 +0200 Subject: [PATCH] Initial commit. --- .gitignore | 1 + Makefile | 6 ++ exit_codes.py | 1 + main.py | 215 +++++++++++++++++++++++++++++++++++++++++++++ sqlite-to-cpp.1.in | 37 ++++++++ 5 files changed, 260 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 exit_codes.py create mode 100755 main.py create mode 100644 sqlite-to-cpp.1.in diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e28fa89 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +sqlite-to-cpp.1 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..982709b --- /dev/null +++ b/Makefile @@ -0,0 +1,6 @@ +sqlite-to-cpp.1: sqlite-to-cpp.1.in + sed \ + -e 's/@@version@@/v0.1/g' \ + -e "s/@@date@@/$$(date -I)/g" \ + $< > $@ + diff --git a/exit_codes.py b/exit_codes.py new file mode 100644 index 0000000..4d581bc --- /dev/null +++ b/exit_codes.py @@ -0,0 +1 @@ +SQL_ERROR = 2 diff --git a/main.py b/main.py new file mode 100755 index 0000000..b99ad9a --- /dev/null +++ b/main.py @@ -0,0 +1,215 @@ +#!/usr/bin/env python3 + +from dataclasses import dataclass +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') + """) + 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 main(): + """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() + + return_value = 0 + + db = sqlite3.connect(':memory:') + cx = db.cursor() + + cx.executescript(args.schema.read()) + + 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)) + + lines.extend([ + f'}}; /* namespace {trans_table(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 args.warning_directives: + lines.append(f'#warning "{PRGR_NAME}: {e}"') + lines.append('') + + body = '\n'.join(lines) + + if not args.no_namespace: + body = f'namespace {args.namespace} {{\n{body}\n}}\n' + + guard = args.header_guard \ + or ('SHA256' + hashlib.sha256(body.encode('UTF-8')).hexdigest()) + + body = f""" +#ifndef {guard} +#define {guard} + +{body} +#endif /* {guard} */ + """ + + print(body.strip(), file=args.output) + + return return_value + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/sqlite-to-cpp.1.in b/sqlite-to-cpp.1.in new file mode 100644 index 0000000..db220af --- /dev/null +++ b/sqlite-to-cpp.1.in @@ -0,0 +1,37 @@ +.TH sqlite-to-cpp 1 "@@date@@" "@@version@@" + +.SH NAME + +sqlite-to-cpp \- Build C++ enums from SQLite databases. + +.SH SYNOPSIS + +.B sqlite-to-cpp +.B [\-b \fIoutput\/\fP] [\| "other options" \|] +.IR schema-file + +.SH DESCRIPTION + +\" All command line arguments + +.TP +\fB\-o\fR, \fB\-\-output\fR +Output file. Where to place the generated C++ code. +Defaults to standard out. + +.SS Exit status + +\" Exit status + +.SH EXAMPLES + +.SH AUTHOR +Written by Hugo Hörnquist <hugo@lysator.liu.se>. + +.SH REPORTING BUGS + +.sh COPYRIGHT + +.sh SEE ALSO + +\" Git Upstream -- GitLab