#!/bin/lua5.4
function read_database_schema(database_file_path, database_name)
print("Reading databse schema from "..database_file_path.." ...")
local file,error = io.open(database_file_path)
assert(file, error)
local context = {
stage = "find",
t = {}
}
local lineno = 0
local line = nil
while true do
line = file:read()
if not line then break end
lineno = lineno + 1
process_line(line, context, lineno)
end
file:close()
return context
end
function expect_eq(line, expect, lineno)
if line ~= expect then
print("Line "..lineno..": expected specifically formatted call:")
print("Expected: »"..expect.."«")
print("Found: »"..line.."«")
return false
end
return true
end
function process_line(line, context, lineno)
if context.stage == "find" then
local table = line:match("^\t\tinfo!%(\"Table: ([^ ]*)")
if table then
print("Found definition for table "..table.." on line "..lineno)
context.stage = "expect execute"
context.current_table = table
end
elseif context.stage == "expect execute" then
local expect = "\t\tself.connection().execute(\""
if expect_eq(line, expect, lineno) then
context.stage = "expect create"
else
os.exit()
end
elseif (context.stage == "parse field" or context.stage == "expect create") and (line:match("^%s*%-%-") or line:match("^%s*$")) then
elseif context.stage == "expect create" then
if line:match("CREATE TABLE IF NOT EXISTS") then
local expect = "\t\t\tCREATE TABLE IF NOT EXISTS "..context.current_table.." ("
if expect_eq(line, expect, lineno) then
context.stage = "parse field"
context.t[context.current_table] = {}
context.ct = context.t[context.current_table]
else
os.exit()
end
elseif line:match("CREATE VIRTUAL TABLE") then
local using = line:match("USING ([^ ]+)")
local expect = "\t\t\tCREATE VIRTUAL TABLE "..context.current_table.." USING "..using.." ("
if expect_eq(line, expect, lineno) then
context.stage = "parse field"
context.t[context.current_table] = {}
context.ct = context.t[context.current_table]
else
os.exit()
end
else
print("Line "..lineno..": Expected start of SQL statement for creating a table.")
end
elseif context.stage == "parse field" and line:match("%);?\"") then
print("End of definition for table "..context.current_table.." on line "..lineno)
if context.last_line_was_comma_ended == true then
print("Stray trailing comma found before end of table definition.\nRemove it, it is not allowed in SQL.")
os.exit(1)
end
context.stage = "find"
context.current_table = nil
context.ct = nil
context.last_line_was_comma_ended = nil
elseif context.stage == "parse field" and line:match("^%s+CHECK%(") then
if context.last_line_was_comma_ended == false then
print("Line "..lineno..": The previous line was not ended with a comma, but a CHECK clause was found in this line, please add the missing comma.")
end
context.last_line_was_comma_ended = not not line:gsub("%s%-%-.*$",""):match(",$")
elseif context.stage == "parse field" and line:match("^%s+UNIQUE%(") then
if context.last_line_was_comma_ended == false then
print("Line "..lineno..": The previous line was not ended with a comma, but a UNIQUE clause was found in this line, please add the missing comma.")
end
context.last_line_was_comma_ended = not not line:gsub("%s%-%-.*$",""):match(",$")
elseif context.stage == "parse field" then
local name, datatype = line:match("^\t*([a-z][a-z0-9_]+)%s+([^ ]+)")
if not name then
if not line:match("^\t*[a-z0-9]+%s*=") then
print("Line "..lineno..": Expected empty line, comment or a table column definition or end of table.")
os.exit(1)
end
end
if context.last_line_was_comma_ended == false then
print("Line "..lineno..": The previous line was not ended with a comma, but a column definition was found in this line, please add the missing comma.")
os.exit(1)
end
if name then
local is_nullable = not line:match("NOT NULL")
if is_nullable and not line:match("NULL") then
print("Line "..lineno..": Expected nullability information, either NULL or NOT NULL")
os.exit(1)
end
context.ct[name] = {
type = datatype,
nullable = is_nullable,
}
end
context.last_line_was_comma_ended = not not line:gsub("%s%-%-.*$",""):match(",$")
end
return context
end
function snake_to_camel(text)
return text:gsub("^.", string.upper):gsub("[_.](.)", string.upper)
end
function get_keys_sorted(t)
local list = {}
for k,_ in pairs(t) do
list[#list+1] = k
end
table.sort(list)
return list
end
function generate_field_enum_for_table(fields, table_name)
local out = ""
local field_names = get_keys_sorted(fields)
out = out.."#[derive(Debug, Clone, PartialEq, Eq)]\n"
out = out.."pub enum "..snake_to_camel(table_name).."Field {\n"
for _,field in ipairs(field_names) do
out = out.."\t"..snake_to_camel(field)..",\n"
end
out = out.."}\n"
out = out.."\n"
out = out.."impl "..snake_to_camel(table_name).."Field {\n"
out = out.."\tpub fn sql_safe_field_name(&self) -> &str {\n"
out = out.."\t\tmatch self {\n"
for _,field in ipairs(field_names) do
out = out.."\t\t\tSelf::"..snake_to_camel(field).." => \""..field.."\",\n"
end
out = out.."\t\t}\n"
out = out.."\t}\n"
out = out.."}\n"
return out
end
function generate_field_enums_for_schema(schema, database)
local out = ""
out = out .. "////////////////////////////////////////////////////////////////\n"
out = out .. "// "..database.." Fields\n"
local tables = get_keys_sorted(schema.t)
for _,table_name in ipairs(tables) do
out = out.."\n\n"
out = out..generate_field_enum_for_table(schema.t[table_name], table_name)
end
return out
end
function generate_table_enum_for_schema(schema, database)
local out = ""
local table_names = get_keys_sorted(schema.t)
out = out.."#[derive(Debug, Clone, PartialEq, Eq)]\n"
out = out.."pub enum "..snake_to_camel(database).."Table {\n"
if database ~= "Base" then
out = out.."\tBase(BaseTable),\n"
end
for _,table in ipairs(table_names) do
out = out.."\t"..snake_to_camel(table)..",\n"
end
out = out.."}\n"
out = out.."\n"
out = out.."impl Table for "..snake_to_camel(database).."Table {\n"
out = out.."\tfn sql_safe_table_name(&self) -> &str {\n"
out = out.."\t\tmatch self {\n"
if database ~= "Base" then
out = out.."\t\t\tSelf::Base(t) => t.sql_safe_table_name(),\n"
end
for _,table in ipairs(table_names) do
out = out.."\t\t\tSelf::"..snake_to_camel(table).." => \""..table.."\",\n"
end
out = out.."\t\t}\n"
out = out.."\t}\n"
out = out.."}\n"
return out
end
function generate_schema_enum_for_schema(schema, base_schema, database)
local out = ""
local table_names = get_keys_sorted(schema.t)
local base_table_names = {}
if base_schema then
base_table_names = get_keys_sorted(base_schema.t)
end
local camel_db = snake_to_camel(database)
out = out.."#[derive(Debug, Clone, PartialEq, Eq)]\n"
out = out.."pub enum "..camel_db.."Schema {\n"
for _,table in ipairs(base_table_names) do
out = out.."\t"..snake_to_camel(table).."("..snake_to_camel(table).."Field),\n"
end
for _,table in ipairs(table_names) do
out = out.."\t"..snake_to_camel(table).."("..snake_to_camel(table).."Field),\n"
end
out = out.."}\n"
out = out.."\n"
out = out.."impl Field for "..snake_to_camel(database).."Schema {\n\n"
out = out.."\ttype TableType = "..snake_to_camel(database).."Table;\n\n"
out = out.."\tfn sql_safe_field_name(&self) -> &str {\n"
out = out.."\t\tmatch self {\n"
for _,table in ipairs(base_table_names) do
out = out.."\t\t\tSelf::"..snake_to_camel(table).."(f) => f.sql_safe_field_name(),\n"
end
for _,table in ipairs(table_names) do
out = out.."\t\t\tSelf::"..snake_to_camel(table).."(f) => f.sql_safe_field_name(),\n"
end
out = out.."\t\t}\n"
out = out.."\t}\n\n"
out = out.."\tfn table(&self) -> &Self::TableType {\n"
out = out.."\t\tmatch self {\n"
for _,table in ipairs(base_table_names) do
out = out.."\t\t\tSelf::"..snake_to_camel(table).."(_) => &"..camel_db.."Table::Base(BaseTable::"..snake_to_camel(table).."),\n"
end
for _,table in ipairs(table_names) do
out = out.."\t\t\tSelf::"..snake_to_camel(table).."(_) => &"..camel_db.."Table::"..snake_to_camel(table)..",\n"
end
out = out.."\t\t}\n"
out = out.."\t}\n\n"
out = out.."}\n"
for _,table in ipairs(base_table_names) do
out = out.."\n"
out = out.."impl From<"..snake_to_camel(table).."Field> for "..snake_to_camel(database).."Schema {\n"
out = out.."\tfn from(value: "..snake_to_camel(table).."Field) -> Self {\n"
out = out.."\t\tSelf::"..snake_to_camel(table).."(value)\n"
out = out.."\t}\n"
out = out.."}\n"
end
for _,table in ipairs(table_names) do
out = out.."\n"
out = out.."impl From<"..snake_to_camel(table).."Field> for "..snake_to_camel(database).."Schema {\n"
out = out.."\tfn from(value: "..snake_to_camel(table).."Field) -> Self {\n"
out = out.."\t\tSelf::"..snake_to_camel(table).."(value)\n"
out = out.."\t}\n"
out = out.."}\n"
end
return out.."\n\n"
end
function write_to_file(file_path, text)
local file,error = io.open(file_path, "w")
assert(file, error)
file:write(text)
file:close()
end
local base_schema = read_database_schema("src/database/base.rs")
local crawler_schema = read_database_schema("src/database/crawler/initalize.rs")
local summary_schema = read_database_schema("src/database/summary/initalize.rs")
local table_file = [[
// DO NOT MODIFY BY HAND!
// This was generated by generate_database_fields.lua
//
// Regenerate if the database tables have changed.
use criterium::sql::Table;
]]
table_file = table_file..generate_table_enum_for_schema(base_schema, "Base")
table_file = table_file..generate_table_enum_for_schema(crawler_schema, "Crawler")
table_file = table_file..generate_table_enum_for_schema(summary_schema, "Summary")
local field_file = [[
// DO NOT MODIFY BY HAND!
// This was generated by generate_database_fields.lua
//
// Regenerate if the database tables have changed.
]]
field_file = field_file..generate_field_enums_for_schema(base_schema, "Base")
field_file = field_file..generate_field_enums_for_schema(crawler_schema, "Crawler")
field_file = field_file..generate_field_enums_for_schema(summary_schema, "Summary")
schema_file = [[
// DO NOT MODIFY BY HAND!
// This was generated by generate_database_fields.lua
//
// Regenerate if the database tables have changed.
use criterium::sql::Field;
use crate::database::BaseTable;
use crate::database::CrawlerTable;
use crate::database::SummaryTable;
use crate::database::fields::*;
]]
schema_file = schema_file..generate_schema_enum_for_schema(base_schema, nil, "Base")
schema_file = schema_file..generate_schema_enum_for_schema(crawler_schema, base_schema, "Crawler")
schema_file = schema_file..generate_schema_enum_for_schema(summary_schema, base_schema, "Summary")
write_to_file("src/database/tables.rs", table_file)
write_to_file("src/database/fields.rs", field_file)
write_to_file("src/database/schemas.rs", schema_file)