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