2019-07-24 18:29:03 +02:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
|
|
|
from mako.template import Template
|
|
|
|
|
2019-09-10 15:50:14 +02:00
|
|
|
import re
|
2019-07-24 18:29:03 +02:00
|
|
|
import sys
|
|
|
|
|
|
|
|
|
2019-09-10 15:50:14 +02:00
|
|
|
DEBUG = False
|
|
|
|
|
|
|
|
|
|
|
|
def eprint(*args, **kwargs):
|
|
|
|
if not DEBUG:
|
|
|
|
return
|
2019-11-02 12:59:25 +01:00
|
|
|
print(*args, file=sys.stderr, **kwargs)
|
2019-09-10 15:50:14 +02:00
|
|
|
|
|
|
|
|
|
|
|
class Rewriter(object):
|
|
|
|
|
|
|
|
def rewrite_types(self, query, mapping):
|
|
|
|
for old, new in mapping.items():
|
|
|
|
query = re.sub(old, new, query)
|
|
|
|
return query
|
|
|
|
|
|
|
|
def rewrite_single(self, query):
|
2019-07-24 18:29:03 +02:00
|
|
|
return query
|
|
|
|
|
2019-09-10 15:50:14 +02:00
|
|
|
def rewrite(self, queries):
|
|
|
|
for i, q in enumerate(queries):
|
2021-11-14 19:03:46 +01:00
|
|
|
if q['name'] is None:
|
|
|
|
continue
|
2019-09-10 15:50:14 +02:00
|
|
|
org = q['query']
|
|
|
|
queries[i]['query'] = self.rewrite_single(org)
|
|
|
|
eprint("Rewritten statement\n\tfrom {}\n\t to {}".format(org, q['query']))
|
|
|
|
return queries
|
|
|
|
|
|
|
|
|
|
|
|
class Sqlite3Rewriter(Rewriter):
|
|
|
|
def rewrite_single(self, query):
|
2021-10-29 13:00:09 +02:00
|
|
|
# Replace DB specific queries with a no-op
|
|
|
|
if "/*PSQL*/" in query:
|
|
|
|
return "UPDATE vars SET intval=1 WHERE name='doesnotexist'" # Return a no-op
|
|
|
|
|
2019-09-10 15:50:14 +02:00
|
|
|
typemapping = {
|
|
|
|
r'BIGINT': 'INTEGER',
|
|
|
|
r'BIGINTEGER': 'INTEGER',
|
|
|
|
r'BIGSERIAL': 'INTEGER',
|
|
|
|
r'CURRENT_TIMESTAMP\(\)': "strftime('%s', 'now')",
|
|
|
|
r'INSERT INTO[ \t]+(.*)[ \t]+ON CONFLICT.*DO NOTHING;': 'INSERT OR IGNORE INTO \\1;',
|
2020-02-18 01:00:58 +01:00
|
|
|
# Rewrite "decode('abcd', 'hex')" to become "x'abcd'"
|
|
|
|
r'decode\((.*),\s*[\'\"]hex[\'\"]\)': 'x\\1',
|
2021-10-13 05:43:13 +02:00
|
|
|
# GREATEST() of multiple columns is simple MAX in sqlite3.
|
|
|
|
r'GREATEST\(([^)]*)\)': "MAX(\\1)",
|
2022-08-15 21:38:46 +02:00
|
|
|
# NULLS FIRST is default behavior on sqlite, make it disappear
|
|
|
|
r' NULLS FIRST': '',
|
2019-09-10 15:50:14 +02:00
|
|
|
}
|
|
|
|
return self.rewrite_types(query, typemapping)
|
|
|
|
|
|
|
|
|
|
|
|
class PostgresRewriter(Rewriter):
|
|
|
|
def rewrite_single(self, q):
|
2021-10-29 13:00:09 +02:00
|
|
|
# Replace DB specific queries with a no-op
|
|
|
|
if "/*SQLITE*/" in q:
|
|
|
|
return "UPDATE vars SET intval=1 WHERE name='doesnotexist'" # Return a no-op
|
|
|
|
|
2019-09-10 15:50:14 +02:00
|
|
|
# Let's start by replacing any eventual '?' placeholders
|
|
|
|
q2 = ""
|
|
|
|
count = 1
|
|
|
|
for c in q:
|
|
|
|
if c == '?':
|
|
|
|
c = "${}".format(count)
|
|
|
|
count += 1
|
|
|
|
q2 += c
|
|
|
|
query = q2
|
|
|
|
|
|
|
|
typemapping = {
|
|
|
|
r'BLOB': 'BYTEA',
|
2022-11-24 17:55:04 +01:00
|
|
|
r'_ROWID_': '(((ctid::text::point)[0]::bigint << 32) | (ctid::text::point)[1]::bigint)', # Yeah, I know...
|
2019-09-10 15:50:14 +02:00
|
|
|
r'CURRENT_TIMESTAMP\(\)': "EXTRACT(epoch FROM now())",
|
|
|
|
}
|
|
|
|
|
|
|
|
query = self.rewrite_types(query, typemapping)
|
|
|
|
return query
|
2019-07-24 18:29:03 +02:00
|
|
|
|
2019-09-02 18:32:15 +02:00
|
|
|
|
2019-07-24 18:29:03 +02:00
|
|
|
rewriters = {
|
|
|
|
"sqlite3": Sqlite3Rewriter(),
|
2019-09-02 18:32:15 +02:00
|
|
|
"postgres": PostgresRewriter(),
|
2019-07-24 18:29:03 +02:00
|
|
|
}
|
|
|
|
|
2021-11-14 18:53:46 +01:00
|
|
|
|
|
|
|
# djb2 is simple and effective: see http://www.cse.yorku.ca/~oz/hash.html
|
|
|
|
def hash_djb2(string):
|
|
|
|
val = 5381
|
|
|
|
for s in string:
|
|
|
|
val = ((val * 33) & 0xFFFFFFFF) ^ ord(s)
|
|
|
|
return val
|
|
|
|
|
|
|
|
|
|
|
|
def colname_htable(query):
|
|
|
|
assert query.upper().startswith("SELECT")
|
|
|
|
colquery = query[6:query.upper().index(" FROM ")]
|
|
|
|
colnames = colquery.split(',')
|
|
|
|
|
|
|
|
# If split caused unbalanced brackets, it's complex: assume
|
|
|
|
# a single field!
|
|
|
|
if any([colname.count('(') != colname.count(')') for colname in colnames]):
|
|
|
|
return [('"' + colquery.strip() + '"', 0)]
|
|
|
|
|
|
|
|
# 50% density htable
|
|
|
|
tablesize = len(colnames) * 2 - 1
|
|
|
|
table = [("NULL", -1)] * tablesize
|
|
|
|
for colnum, colname in enumerate(colnames):
|
|
|
|
colname = colname.strip()
|
|
|
|
# SELECT xxx AS yyy -> Y
|
2022-07-19 07:05:26 +02:00
|
|
|
as_clause = colname.upper().rfind(" AS ")
|
2021-11-14 18:53:46 +01:00
|
|
|
if as_clause != -1:
|
|
|
|
colname = colname[as_clause + 4:].strip()
|
|
|
|
|
|
|
|
pos = hash_djb2(colname) % tablesize
|
|
|
|
while table[pos][0] != "NULL":
|
|
|
|
pos = (pos + 1) % tablesize
|
|
|
|
table[pos] = ('"' + colname + '"', colnum)
|
|
|
|
return table
|
|
|
|
|
|
|
|
|
2019-07-24 18:29:03 +02:00
|
|
|
template = Template("""#ifndef LIGHTNINGD_WALLET_GEN_DB_${f.upper()}
|
|
|
|
#define LIGHTNINGD_WALLET_GEN_DB_${f.upper()}
|
|
|
|
|
|
|
|
#include <config.h>
|
2021-11-14 18:53:46 +01:00
|
|
|
#include <ccan/array_size/array_size.h>
|
2022-01-03 19:45:35 +01:00
|
|
|
#include <db/common.h>
|
|
|
|
#include <db/utils.h>
|
2019-07-24 18:29:03 +02:00
|
|
|
|
|
|
|
#if HAVE_${f.upper()}
|
2021-11-14 18:53:46 +01:00
|
|
|
% for colname, table in colhtables.items():
|
|
|
|
static const struct sqlname_map ${colname}[] = {
|
|
|
|
% for t in table:
|
|
|
|
{ ${t[0]}, ${t[1]} },
|
|
|
|
% endfor
|
|
|
|
};
|
|
|
|
|
|
|
|
% endfor
|
2019-07-24 18:29:03 +02:00
|
|
|
|
2021-11-14 19:03:46 +01:00
|
|
|
const struct db_query db_${f}_queries[] = {
|
2019-07-24 18:29:03 +02:00
|
|
|
|
|
|
|
% for elem in queries:
|
|
|
|
{
|
2021-11-14 19:03:46 +01:00
|
|
|
% if elem['name'] is not None:
|
2019-07-24 18:29:03 +02:00
|
|
|
.name = "${elem['name']}",
|
|
|
|
.query = "${elem['query']}",
|
2019-07-25 18:19:16 +02:00
|
|
|
.placeholders = ${elem['placeholders']},
|
|
|
|
.readonly = ${elem['readonly']},
|
2021-11-14 18:53:46 +01:00
|
|
|
% if elem['colnames'] is not None:
|
|
|
|
.colnames = ${elem['colnames']},
|
|
|
|
.num_colnames = ARRAY_SIZE(${elem['colnames']}),
|
2021-11-14 19:03:46 +01:00
|
|
|
% endif
|
2021-11-14 18:53:46 +01:00
|
|
|
% endif
|
2019-07-24 18:29:03 +02:00
|
|
|
},
|
|
|
|
% endfor
|
|
|
|
};
|
|
|
|
|
2022-03-01 19:24:18 +01:00
|
|
|
struct db_query_set ${f}_query_set = {
|
|
|
|
.name = "${f}",
|
|
|
|
.query_table = db_${f}_queries,
|
|
|
|
.query_table_size = ARRAY_SIZE(db_${f}_queries),
|
|
|
|
};
|
|
|
|
|
|
|
|
AUTODATA(db_queries, &${f}_query_set);
|
2019-07-24 18:29:03 +02:00
|
|
|
#endif /* HAVE_${f.upper()} */
|
|
|
|
|
|
|
|
#endif /* LIGHTNINGD_WALLET_GEN_DB_${f.upper()} */
|
|
|
|
""")
|
|
|
|
|
|
|
|
|
2021-11-14 19:03:46 +01:00
|
|
|
def queries_htable(queries):
|
|
|
|
# Converts a list of queries into a hash table.
|
|
|
|
tablesize = len(queries) * 2 - 1
|
|
|
|
htable = [{'name': None}] * tablesize
|
|
|
|
|
|
|
|
for q in queries:
|
|
|
|
pos = hash_djb2(q['name']) % tablesize
|
|
|
|
while htable[pos]['name'] is not None:
|
|
|
|
pos = (pos + 1) % tablesize
|
|
|
|
htable[pos] = q
|
|
|
|
|
|
|
|
return htable
|
|
|
|
|
|
|
|
|
2019-07-24 18:29:03 +02:00
|
|
|
def extract_queries(pofile):
|
|
|
|
# Given a po-file, extract all queries and their associated names, and
|
|
|
|
# return them as a list.
|
|
|
|
|
|
|
|
def chunk(pofile):
|
|
|
|
# Chunk a given file into chunks separated by an empty line
|
|
|
|
with open(pofile, 'r') as f:
|
|
|
|
chunk = []
|
|
|
|
for line in f:
|
|
|
|
line = line.strip()
|
|
|
|
if line.strip() == "":
|
|
|
|
yield chunk
|
|
|
|
chunk = []
|
|
|
|
else:
|
|
|
|
chunk.append(line.strip())
|
|
|
|
if chunk != []:
|
|
|
|
yield chunk
|
|
|
|
|
2021-11-14 18:53:46 +01:00
|
|
|
colhtables = {}
|
2019-07-24 18:29:03 +02:00
|
|
|
queries = []
|
|
|
|
for c in chunk(pofile):
|
|
|
|
|
|
|
|
# Skip other comments
|
|
|
|
i = 1
|
|
|
|
while c[i][0] == '#':
|
|
|
|
i += 1
|
|
|
|
|
|
|
|
# Strip header and surrounding quotes
|
|
|
|
query = c[i][7:][:-1]
|
|
|
|
|
2021-11-14 18:53:46 +01:00
|
|
|
is_select = query.upper().startswith("SELECT")
|
|
|
|
if is_select:
|
|
|
|
colnames = 'col_table{}'.format(len(queries))
|
|
|
|
colhtables[colnames] = colname_htable(query)
|
|
|
|
else:
|
|
|
|
colnames = None
|
|
|
|
|
2019-07-24 18:29:03 +02:00
|
|
|
queries.append({
|
2019-09-05 16:08:54 +02:00
|
|
|
'name': query,
|
2019-07-24 18:29:03 +02:00
|
|
|
'query': query,
|
|
|
|
'placeholders': query.count('?'),
|
2021-11-14 18:53:46 +01:00
|
|
|
'readonly': "true" if is_select else "false",
|
|
|
|
'colnames': colnames,
|
2019-07-24 18:29:03 +02:00
|
|
|
})
|
2021-11-14 19:03:46 +01:00
|
|
|
return colhtables, queries_htable(queries)
|
2019-07-24 18:29:03 +02:00
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
if len(sys.argv) != 3:
|
|
|
|
print("Usage:\n\t{} <statements.po-file> <output-dialect>".format(sys.argv[0]))
|
|
|
|
sys.exit(1)
|
|
|
|
|
|
|
|
dialect = sys.argv[2]
|
|
|
|
|
|
|
|
if dialect not in rewriters:
|
|
|
|
print("Unknown dialect {}. The following are available: {}".format(
|
|
|
|
dialect,
|
|
|
|
", ".join(rewriters.keys())
|
|
|
|
))
|
|
|
|
sys.exit(1)
|
|
|
|
|
|
|
|
rewriter = rewriters[dialect]
|
|
|
|
|
2021-11-14 18:53:46 +01:00
|
|
|
colhtables, queries = extract_queries(sys.argv[1])
|
2019-07-24 18:29:03 +02:00
|
|
|
queries = rewriter.rewrite(queries)
|
|
|
|
|
2021-11-14 18:53:46 +01:00
|
|
|
print(template.render(f=dialect, queries=queries, colhtables=colhtables))
|