unobtanium 3.0.0

Opinioated Web search engine library with crawler and viewer companion.
Documentation
#!/bin/lua5.4

-- This script reads in the database definitions from src/database/{base,crawler,summary}.rs
-- and generates the needed type definitions for use with criterium from that.

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
		-- comment, do nothing
	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
		-- check
		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
		-- unique
		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)