#!/usr/bin/env python
# This script should be compatible with both python 2 and python 3

import re
import sys
import os
import subprocess
import copy
from functools import cmp_to_key

pkg_name = "Orm"

store_connections = True
# If True, connections are stored in detached objects (and need to be passed
# explicitly to Get). If False, the connections are queried from a pool
# automatically otherwise

database_connection = "Session_Type"
get_db_connection = ".DB"
# These two variables indicate the type used for the database connection.
# database_connection is what is stored inside lists, and get_db_connection
# is the suffix to append to get the actual
# GNATCOLL.SQL.Exec.Database_Connection
# Only relevant if store_connections is True

max_depth = 3
# Maximum valid value for Select_Related
# No need in putting this too high, since that will generate make queries much
# bigger, thus invalidating any saving we might have in doing only one of them

database_pkg = "Database"
# Name of the package that contains the database description

debug = False
# Insert additional debug code in the generated source


def exec_or_fail(*args, **kwargs):
    """Same parameters as process.Process, but prints an error message in
    case of error, and then raises an exception
    """

    try:
        sub = subprocess.Popen(*args, **kwargs)
        sub.wait()
        if sub.returncode != 0:
            print("Error: could not execute %s" % args[0])
            raise subprocess.CalledProcessError(sub.returncode, args[0])

        return sub

    except OSError as E:
        print("Error: could not execute %s" % args[0])
        raise subprocess.CalledProcessError(sub.returncode, args[0]) from E


def save_dir(fn):
    """Temporarily change the current directory while running a function,
    and restore it when the function exits. This is a decorator:
       @save_dir
       def my_function ():
          ...
    """

    def do_work(*args, **kwargs):
        saved = os.getcwd()
        try:
            result = fn(*args, **kwargs)
        finally:
            os.chdir(saved)
        return result

    do_work.__name__ = fn.__name__  # Reset name
    do_work.__doc__ = fn.__doc__
    return do_work


def unlink_if_exist(files):
    """Unlink, if they exist, the file(s).
    No error is propagated if the files do not exist
    """

    if not isinstance(files, list):
        files = [files]
    for f in files:
        try:
            os.unlink(f)
        except OSError:
            pass


def max_length(iter):
    """Return the length of the longuest element in iter"""
    longuest = 0
    for f in iter:
        longuest = max(longuest, len(f))
    return longuest


def splitstr(str, maxlen):
    """Split str into lines at most maxlen characters wide"""
    line = ""
    result = []
    for w in str.split():
        if len(line) + len(w) + 1 > maxlen:
            result.append(line)
            line = w
        else:
            line += " " + w

    if line != "":
        result.append(line)

    return result


######################################
# Pretty-printer
######################################


class Subprogram:
    __slots__ = (
        "name",
        "params",
        "local_vars",
        "body",
        "returns",
        "comment",
        "overriding",
        "abstract",
        "inline",
    )

    def __init__(
        self,
        name,
        params,
        local_vars,
        body,
        returns,
        comment,
        overriding,
        abstract,
        inline,
    ):
        self.name = name
        self.params = params
        self.local_vars = local_vars
        self.body = body
        self.returns = returns
        self.comment = comment
        self.overriding = overriding
        self.abstract = abstract
        self.inline = inline


def cmp(a, b):
    return (a > b) - (a < b)


def _subprogram_sorter(sub1, sub2):
    """cmd for two subprograms.
    This groups property setters/getters together
    """
    n1 = sub1.name.replace("set_", "")
    n2 = sub2.name.replace("set_", "")
    return cmp(n1, n2)


class Pretty_Printer(object):
    """This class is responsible for doing the actual output of code, properly
    formatted
    """

    def __init__(self, out, casing=None):
        """CASING stored the casing exceptions. This is a list of names with
        their expected formating. All names not in that list will be
        capitalized
        """
        if casing is None:
            casing = []

        casing.extend(["out", "in", "access", "constant", "aliased"])
        self.out = out
        self.casing = {}
        for c in casing:
            self.casing[c.lower()] = c
        self.pkg_name = None

    def start_package(self, pkg_name):
        """Terminate the current file, do the output, and start a new file"""
        self.terminate_package(need_dba=True)
        self.pkg_name = pkg_name.title()
        self.spec_withs = []  # List of packages to with+use in specs
        self.body_withs = []  # List of packages to with+use in bodies
        self.global_vars = []  # Global variables and constants
        self.body_cst = []  # Constants to put in the body, same as params
        self.unique_body_cst = []
        self.private_before = ""  # Private part of the spec
        self.private_after = ""  # Private part of the spec
        self.body_code = ""  # Goes in body before subprograms
        self.sections = []  # Sections in the specs. Contains tuples:
        #                     0 => section name
        #                     1 => types declaration for the section
        #                     2 => list of subprograms:
        #                          (name, params, body, returns, comment)
        self.sections.append(("", "", [], ""))  # Default subprograms

    def add_with(self, pkg, specs=True, do_use=True):
        """Add a with+use clause for pkg. pkg can be a list of packages.
        Automatic casing is performed. If specs is True, the withs are
        appended to the specs, otherwise to the body"""
        if type(pkg) == str:
            pkg = [pkg]

        for p in pkg:
            if specs:
                if (p, do_use) not in self.spec_withs:
                    self.spec_withs.append((p, do_use))
            else:
                if (p, do_use) not in self.body_withs:
                    self.body_withs.append((p, do_use))

    def add_constants(self, cst):
        """cst has the same format as the parameters of a subprogram"""
        if cst:
            self.body_cst.append(cst)

    def add_unique_constants(self, cst):
        """Same as add_constants, does nothing if the constant already exists"""
        if cst:
            for c in cst:
                if c not in self.unique_body_cst:
                    self.unique_body_cst.append(c)

    def add_property(
        self,
        schema,
        row,
        field,
        getter,
        setter,
        type,
        getter_local_vars=None,
        setter_local_vars=None,
        comment=None,
        abstract=False,
        section="",
    ):
        """Define a property for self (ie a setter and getter strongly linked
        to each other
        """
        if getter_local_vars is None:
            getter_local_vars = []
        if setter_local_vars is None:
            setter_local_vars = []

        get_name = schema.subprogram_name_from_field(field)
        if isinstance(field, str):
            set_name = "set_%s" % field
        else:
            set_name = "set_%s" % field.name

        if getter:
            self.add_subprogram(
                get_name,
                body=getter,
                params=[("self", "detached_%s" % row)],
                local_vars=getter_local_vars,
                returns=type,
                comment=comment,
                abstract=abstract,
                section=section,
            )

        if setter:
            self.add_subprogram(
                set_name,
                body=setter,
                local_vars=setter_local_vars,
                params=[("self", "detached_%s" % row), ("value", type)],
                comment=comment,
                abstract=abstract,
                section=section,
            )

    def add_subprogram(
        self,
        name,
        body,
        params=None,
        local_vars=None,
        returns=None,
        comment=None,
        overriding=False,
        abstract=False,
        inline=False,
        section="",
    ):
        """Add a new subprogram. local_vars and params are lists of
        (name, type, default) tuples.
         The body will be pretty-printed automatically, and the required
         pragma Unreferenced are also added automatically.
         If the section is "body" the subprogram will not be visible in the
         specs
        """
        if params is None:
            params = []
        if local_vars is None:
            local_vars = []

        news = Subprogram(
            name,
            params,
            local_vars,
            body,
            returns,
            comment,
            overriding,
            abstract,
            inline,
        )

        for index, s in enumerate(self.sections):
            if s[0] == section:
                self.sections[index] = (section, s[1], s[2] + [news], s[3])
                return
        self.sections.append((section, "", [news], ""))

    def add_section(self, name, types, comment=""):
        """Add a new section in the specs, which contains TYPES, as well as
        any subprogram added to that section later on.
        COMMENT is a general comment for the section
        """
        for index, s in enumerate(self.sections):
            if s[0] == name:
                self.sections[index] = (s[0], s[1] + "\n" + types, s[2], s[3])
                return

        self.sections.append((name, types, [], comment))

    def add_private_before(self, str):
        """Add custom contents to the private section, before the subprograms
        in that section"""
        self.private_before += str

    def add_private_after(self, str):
        """Add custom contents to the private section, after the subprograms
        in that section"""
        self.private_after += str

    def _title(self, name):
        def _title_sep(sep, str):
            if sep == ".":
                return [" ".join(_title_sep(" ", n)) for n in str.split(sep)]
            elif sep == " ":
                return ["'".join(_title_sep("'", n)) for n in str.split(sep)]
            else:
                return [self.casing.get(n.lower(), n.title()) for n in str.split(sep)]

        return ".".join(_title_sep(".", name))

    def _output_withs(self, list):
        if list:
            self.out.write("pragma Warnings (Off);\n")
            ml = max_length(list)
            for w, do_use in sorted(list):
                if do_use:
                    self.out.write("with %-*s use %s;\n" % (ml + 1, w + ";", w))
                else:
                    self.out.write("with %-*s;\n" % (ml, w))
            self.out.write("pragma Warnings (On);\n")

    def _format_decl(self, list):
        """List has the same format as params and local_vars for a subprogram"""
        ml = max_length([p[0] for p in list])
        result = []
        for p in list:
            if len(p) == 3:
                name, type, default = p
                result.append(
                    "%-*s : %s := %s"
                    % (ml, self._title(name), self._title(type), default)
                )
            elif len(p) == 2:
                name, type = p
                result.append("%-*s : %s" % (ml, self._title(name), self._title(type)))
            else:
                result.append(p[0])

        return result

    def _format_parameters(self, params):
        result = self._format_decl(params)
        if result:
            return "\n     (" + ";\n      ".join(result) + ")"
        else:
            return ""

    def _format_local_vars(self, local_vars):
        result = self._format_decl(local_vars)
        if result:
            return "      " + ";\n      ".join(result) + ";\n"
        else:
            return ""

    def _get_subprogram_proto(self, subp):
        p = self._format_parameters(subp.params)

        if subp.overriding:
            prefix = "overriding "
        else:
            prefix = ""

        if subp.returns:
            func = "   %sfunction %s%s\n     return %s" % (
                prefix,
                self._title(subp.name),
                p,
                self._title(subp.returns),
            )
        else:
            func = "   %sprocedure %s%s" % (prefix, self._title(subp.name), p)

        if subp.abstract:
            func = func + " is abstract"

        # Would we fit on a single line ?
        if len(re.sub(r"\s+", " ", func)) < 79:
            return "  " + re.sub(r"\s+", " ", func)
        else:
            return func

    def _output_section_spec(self, section):
        name, types, subprograms, comment = section
        if name and name != "body" and name != "private":
            self.out.write("\n   " + "-" * (len(name) + 6) + "\n")
            self.out.write("   -- %s --\n" % self._title(name))
            self.out.write("   " + "-" * (len(name) + 6) + "\n")

            if comment:
                self.out.write("   " + comment.strip() + "\n")

            self.out.write("\n")

        if types:
            self._output_indented(types.rstrip(), indent=3)
            self.out.write("\n")

        # Sort subprograms within the section
        subprograms.sort(key=cmp_to_key(_subprogram_sorter))

        comment = ""

        for index, p in enumerate(subprograms):
            self.out.write(self._get_subprogram_proto(p) + ";\n")

            if p.inline:
                self.out.write("   pragma Inline (%s);\n" % p.name)

            if p.comment:
                comment += " " + p.comment

            # Share the comment with the next subprogram if it has the same
            # name

            if (
                index + 1 < len(subprograms)
                and _subprogram_sorter(subprograms[index + 1], p) != 0
            ):
                if comment:
                    self.out.write("   -- ")
                    self.out.write("\n   --  ".join(splitstr(comment, 72)))
                    self.out.write("\n")
                comment = ""
                self.out.write("\n")

    def _output_subprogram_specs(self):
        for section in self.sections:
            if section[0] == "body":
                pass
            elif section[0] != "private":
                self._output_section_spec(section)

    def _output_indented(self, code, indent=3):
        body = code.strip()
        if not body:
            return

        # Add newlines where needed, but preserve existing blank lines
        body = re.sub(r";(?!\s*\n)", ";\n", body)
        body = re.sub(r"(?<!and )then(?!\s*\n)", "then\n", body)
        body = re.sub(r"(?<!or )else(?!\s*\n)", "else\n", body)
        body = re.sub("declare", "\ndeclare", body)
        body = re.sub(r"\n\s*\n+", "\n\n", body)

        parent_count = 0

        for ln in body.splitlines():
            ln = ln.strip()
            if (
                ln.startswith("end")
                or ln.startswith("elsif")
                or ln.startswith("else")
                or ln.startswith("exception")
                or ln.startswith("begin")
            ):
                indent -= 3

            old_parent = parent_count
            parent_count = parent_count + ln.count("(") - ln.count(")")

            if not ln:
                pass
            elif ln[0] == "(":
                self.out.write(" " * (indent + 2))
                if parent_count > old_parent:
                    indent += (parent_count - old_parent) * 3
            elif not old_parent:
                self.out.write(" " * indent)
                if parent_count > old_parent:
                    indent += (parent_count - old_parent) * 3
            else:
                if parent_count > old_parent:
                    indent += (parent_count - old_parent) * 3
                self.out.write(" " * indent)

            if old_parent > parent_count:
                indent -= (old_parent - parent_count) * 3

            self.out.write(ln)
            self.out.write("\n")

            if (
                (ln.endswith("then") and not ln.endswith("and then"))
                or ln.endswith("loop")
                or (ln.endswith("else") and not ln.endswith("or else"))
                or ln.endswith("begin")
                or ln.endswith("exception")
                or ln.endswith("declare")
            ):
                indent += 3

    def _output_subprogram_bodies(self):
        for section in self.sections:
            if section[0] == "body":
                self._output_section_spec(section)

        subp = []
        for _, _, subprograms, _ in self.sections:
            subp.extend(subprograms)

        subp.sort(key=lambda x: x.name)

        for p in subp:
            if not p.abstract:
                self.out.write("\n   " + "-" * (len(p.name) + 6) + "\n")
                self.out.write("   -- %s --\n" % self._title(p.name))
                self.out.write("   " + "-" * (len(p.name) + 6) + "\n\n")

                proto = self._get_subprogram_proto(p)
                lines = proto.splitlines()
                if not p.local_vars and len(lines[-1]) < 79 - 3:
                    self.out.write(proto + " is\n")
                else:
                    self.out.write(proto + "\n   is\n")

                if p.local_vars:
                    local = self._format_local_vars(p.local_vars)
                else:
                    local = ""

                unreferenced = []
                for param in p.params:
                    if not re.search(
                        r"\b%s\b" % param[0], p.body + local, re.IGNORECASE
                    ):
                        unreferenced.append(self._title(param[0]))

                if unreferenced:
                    self.out.write(
                        "      pragma Unreferenced (%s);\n" % (", ".join(unreferenced))
                    )

                if p.local_vars:
                    self.out.write(local)

                self.out.write("   begin\n")
                self._output_indented(p.body, indent=6)
                self.out.write("   end %s;\n" % (self._title(p.name)))

    def terminate_package(self, need_dba=False):
        """Terminate the current file, and do the output"""
        if not self.pkg_name:
            return

        # Specs

        self._output_withs(self.spec_withs)
        self.out.write("pragma Style_Checks (Off);\n\n")
        self.out.write("package %s is\n" % self.pkg_name)

        if need_dba:
            self.out.write("   package DBA renames %s;\n" % database_pkg)
            self.out.write(
                "   subtype Related_Depth is Integer range 0 .. %d;\n" % max_depth
            )

        self._output_subprogram_specs()
        self.out.write("\nprivate\n")
        self.out.write(self.private_before)
        self.out.write("\n")
        private = [s for s in self.sections if s[0] == "private"]
        for p in private:
            self._output_section_spec(p)
        self.out.write(self.private_after)
        self.out.write("\n")

        self.out.write("end %s;\n\n" % self.pkg_name)

        # Bodies
        self._output_withs(self.body_withs)
        self.out.write("pragma Style_Checks (Off);\n\n")
        self.out.write("package body %s is\n" % self.pkg_name)

        if need_dba:
            self.out.write("   pragma Warnings (Off);\n")
            self.out.write("   use Sessions.Pointers;\n")
            if debug:
                self.out.write('   Me : constant Trace_Handle := Create("ORM");')

        for c in sorted(self.unique_body_cst):
            self.out.write("\n   " + ";\n   ".join(self._format_decl([c])) + ";")

        self.out.write("\n")
        for c in self.body_cst:
            self.out.write("\n   " + ";\n   ".join(self._format_decl(c)) + ";")

        self.out.write(self.body_code)

        if need_dba:
            self.out.write("\n\n")
            self.out.write("   pragma Warnings (On);")
        self.out.write("\n")

        self._output_subprogram_bodies()
        self.out.write("end %s;\n\n" % self.pkg_name)

        self.pkg_name = None


######################################
# Schema
######################################


class Cannot_Parse_Schema(Exception):
    pass


class Schema(object):
    """A class that represents a database schema"""

    #############
    # __init__
    #############

    def __init__(self, setup, tables, pretty, all_tables, omit):
        """Connect to the database and get the schema.
        SETUP is an object which contains the database description.
        OMIT is the list of fields to omit in the binding"""

        self.pretty = pretty

        self.details = get_db_schema(
            setup, omit=omit, requires_pk=True, all_tables=all_tables
        )
        if not self.details:
            raise Cannot_Parse_Schema

        self.sql_tables = None

    #############
    # withs_for
    #############

    def withs_for(self, table):
        """Create the list of withs needed for table"""

        self.pretty.add_with(
            [
                "GNATCOLL.SQL",
                "GNATCOLL.SQL.Exec",
                "GNATCOLL.Tribooleans",
                "GNATCOLL.SQL_Fields",
                "GNATCOLL.SQL.Orm",
                "GNATCOLL.SQL.Orm.Impl",
                "GNATCOLL.SQL.Sessions",
                "Ada.Strings.Unbounded",
                "GNAT.Strings",
                database_pkg.title(),
                "GNAT.Calendar",
                "Ada.Calendar",
                "Ada.Finalization",
            ]
        )
        self.pretty.add_with("Ada.Unchecked_Deallocation", specs=False, do_use=False)
        self.pretty.add_with("Ada.Containers", specs=False)
        self.pretty.add_with("System.Address_Image", do_use=False)

        if not store_connections:
            self.pretty.add_with("Sessions", specs=False)
        if debug:
            self.pretty.add_with("GNATCOLL.Traces")
            self.pretty.add_with("System.Address_Image", do_use=False)

    #################
    # params_create
    #################

    def params_create(self, table):
        """Return the list of parameters to Create, for a given table
        TABLE is an instance of Table"""
        return [(f.name, f.type.ada_param, f.type.default_param) for f in table.fields]

    ##########################
    # Compute the name of the subprogram to use for a field
    # If the field has the same name as one of the tables or rows, we would
    # get conflicts and couldn't compile the code, so we have to generate
    # a different name for the subprogram
    ##########################

    def subprogram_name_from_field(self, field):
        """FIELD is an instance of Field, or a string"""
        if isinstance(field, str):
            base = field
        else:
            base = field.name

        f = base.lower()
        for table in list(self.details.values()):
            if f == table.row.lower():
                return "Get_" + base

        return base

    ######################
    # call_create_params
    ######################

    def call_create_params(self, table):
        """Parameters when calling Create for TABLE"""

        params = []
        for pk in self.details[table].pk:
            params.append("%s => %s" % (pk.name.title(), pk.name.title()))
        return params

    #######
    # equal
    #######

    def equal(self, table):
        """Return the comparison operators when comparing Op1 and Op2
        TABLE is instance of Table
        """

        params = []
        for pk in table.pk:
            params.append(
                "%s'(Op1.%s) = Op2.%s"
                % (
                    pk.type.ada_param,
                    self.subprogram_name_from_field(pk),
                    self.subprogram_name_from_field(pk),
                )
            )
        return params

    #################
    # params_get_pk
    #################

    def params_get_pk(self, table):
        """Return the list of parameters for Get.
        TABLE is an instance of Table"""

        if not table.pk:
            return []

        params = []

        for f in table.pk:
            params.append((f.name, f.type.ada_param))

        return params

    #######################
    # detached_data_fields
    #######################

    def detached_data_fields(self, table):
        """TABLE is instance of Table"""

        longuest = max_length([f.name for f in table.fields])
        data = []

        for f in table.fields:
            if f.show:
                data.append(
                    "       ORM_%-*s : %s := %s"
                    % (longuest + 3, f.name, f.type.ada_field, f.default_for_field())
                )

        for f in table.fk:
            if f.foreign.show:
                name = subprogram_from_fk(f)
                f2 = table.get_field(name)
                if f2 is None or f2.show:
                    data.append(
                        "       ORM_FK_%-*s : Detached_%s_Access := null"
                        % (longuest, name, f.foreign.row)
                    )

        return ";\n".join(sorted(data))

    ##################
    # unchecked_free
    ##################

    def unchecked_free(self, table, all_tables):
        """List of Unchecked_Free instantiations needed for TABLE"""

        d = self.details[table]
        ml = set()

        for f in d.fk:
            if f.show():
                ml.add(
                    (
                        "procedure Unchecked_Free is "
                        + "new Ada.Unchecked_Deallocation\n"
                        + "     (Detached_%(row)s'Class, Detached_%(row)s_Access)"
                        % {"row": f.foreign.row},
                    )
                )

        # The call to set() is to uniquify the elements in the list
        return list(ml)

    ###############
    # free_fields
    ###############

    def free_fields(self, table):
        """The commands to free the fields of the manager"""

        free_fields = []
        for f in table.fk:
            if f.foreign.show:
                subp = subprogram_from_fk(f)
                try:
                    free_fields.append("Unchecked_Free (Self.ORM_FK_%s);" % subp)
                except KeyError:
                    print("free_fields: Invalid field: %s.%s" % (table, subp))

        for f in table.fields:
            free_fields.append(f.free_field("Self"))

        if free_fields:
            return "\n".join(free_fields)
        else:
            return ""


######################
# subprogram_from_fk
######################


def subprogram_from_fk(fk):
    """Return the name of the primitive operation, for a given Foreign Key
    Handling of multi-key foreign keys:
      subscription(nb,type) REFERENCES contract(nb,type)
    Should result in
      function Contract(Self : Subscription) return Contract;
    When the FK has a single key, we use that name for the name of the
    subprogram, which leads to a more natural use
    """

    if len(fk.pairs) == 1:
        return fk.pairs[0][0].name
    else:
        return fk.foreign.name.title()


######################################
# generator for Internal_Query
######################################


def internal_query(pretty, table, schema):
    joins = ""
    lj = "Table"
    body = ""
    fk_list = []
    local_vars = [("Table", "T_Numbered_%s(Aliases(Base))" % table.name)]

    field_list = ["Table.%s" % f.name for f in table.fields]
    fklocal = ""

    for index, fk in enumerate(table.fk):
        if fk.foreign.show:
            reft = "FK%d" % (index + 1,)

            fklocal += "\n%s : T_Numbered_%s(Aliases(Aliases(Base + %d)));" % (
                reft,
                fk.foreign.name,
                index + 1,
            )

            # Join criteria. No need to repeat for null FK, since we already
            # use a LEFT JOIN.

            if not fk.can_be_null():
                for f in fk.pairs:
                    joins += "\nand Table.%(field)s = %(reft)s.%(reff)s" % {
                        "field": f[0].name,
                        "reft": reft,
                        "reff": f[1].name,
                    }

            # Add possible LEFT JOIN

            if fk.can_be_null():
                criteria = []
                for f in fk.pairs:
                    criteria.append(
                        "Table.%(field)s=%(reft)s.%(reff)s"
                        % {"reft": reft, "field": f[0].name, "reff": f[1].name}
                    )

                lj = """Left_Join(%(lj)s, %(reft)s, %(criteria)s)""" % {
                    "lj": lj,
                    "criteria": " and ".join(criteria),
                    "reft": reft,
                }

            # Add fields for FK table

            fk_body = (
                "C2 := No_Criteria;"
                + "Do_Query_%(ref)s(Fields, T, C2,"
                + "Aliases(Base + %(index)d),\n"
                + "Aliases, Depth - 1, Follow_LJ);"
            ) % {"ref": fk.foreign.name, "index": index + 1}

            fk_body += "if Depth > 1 then Criteria := Criteria and C2; end if;"

            if fk.can_be_null():
                fk_body = "if Follow_LJ then %s end if;" % fk_body
            else:
                fk_body += "From := From & T;"
            fk_list.append(fk_body)

    if joins:
        joins = "Criteria := Criteria%s;" % joins

    if fk_list:
        body = "if Depth > 0 then declare %s begin %s" % (fklocal, joins)

        if lj == "Table":
            body += "From := +Table;"
        else:
            body += "if Follow_LJ then From := +%s; else From := +Table; end if;" % lj

        body += "%s end; end if;" % "\n\n".join(fk_list)
        local_vars.extend([("C2", "SQL_Criteria"), ("T", "SQL_Table_List")])
    else:
        fk_list = "From := +Table;"

    pkfield_list = ["Table.%s" % f.name for f in table.fields if f.is_pk()]

    if table.pk != []:
        body = (
            """if PK_Only then
       Fields := Fields & %(pkfields)s;
    else
       Fields := Fields & %(fields)s;
    end if;
    From := Empty_Table_List;"""
            + body
        )
    else:
        body = """Fields := Fields & %(fields)s; """ + body

    body = body % {
        "fields": "\n& ".join(field_list),
        "pkfields": "\n& ".join(pkfield_list),
    }

    if debug:
        tmp = (
            "Trace(Me, \"Do_Query_%s, Base=(\" & Base'Img & Aliases(Base)'Img"
            % table.name
        )
        tmp += '& ")"'
        for index, fk in enumerate(table.fk):
            if fk.foreign.show:
                tmp += (
                    '\n & " FK%(i)d=(" & Aliases(Base + %(i)d)\'Img'
                    + '& Aliases(Aliases(Base + %(i)d))\'Img & ")"'
                ) % {"i": index + 1}

        tmp += ");"

        body = tmp + body

    if table.pk != []:
        params = [("PK_Only", "Boolean", "False")]
        internal_body = """Do_Query_%(cap)s(Fields, From, Criteria,
           0, Alias_%(cap)s, Depth, Follow_LJ, PK_Only);"""
    else:
        params = []
        internal_body = """if PK_Only then
        raise Program_Error with "Table %(cap)s has no primary key";
    end if;
    Do_Query_%(cap)s(Fields, From, Criteria,
       0, Alias_%(cap)s, Depth, Follow_LJ);"""

    pretty.add_subprogram(
        name="internal_query_%s" % table.name,
        params=[
            ("fields", "in out SQL_Field_List"),
            ("from", "out SQL_Table_List"),
            ("criteria", "in out SQL_Criteria"),
            ("depth", "natural"),
            ("Follow_LJ", "Boolean"),
            ("PK_Only", "Boolean", "False"),
        ],
        section="Managers(implementation details)",
        body=internal_body % {"cap": table.name},
    )

    pretty.add_subprogram(
        name="do_query_%s" % table.name,
        params=[
            ("fields", "in out SQL_Field_List"),
            ("from", "out SQL_Table_List"),
            ("criteria", "in out SQL_Criteria"),
            ("Base", "Natural"),
            ("Aliases", "Alias_Array"),
            ("depth", "natural"),
            ("Follow_LJ", "Boolean"),
        ]
        + params,
        local_vars=local_vars,
        section="body",
        body=body,
    )


def create(pretty, table, schema):
    """Generate a Create subprogram to create managers"""
    body = ""
    for f in table.fields:
        body += "if %s /= %s then " % (f.name, f.type.default_param)
        if f.type.sql_type.lower() == "boolean":
            body += " C := C and DBA.%s.%s = To_Boolean(%s);" % (
                table.name,
                f.name,
                f.name,
            )
        else:
            body += " C := C and DBA.%s.%s = %s;" % (table.name, f.name, f.name)
        body += "end if;"

    body += "Copy(Self.Filter(C), Into => Result); return Result;"
    # body += "return %s_Managers(Self.Filter(C));" % table.name

    pretty.add_subprogram(
        name="filter",
        params=[("self", "%s_Managers'Class" % table.name)]
        + schema.params_create(table),
        body=body,
        local_vars=[
            ("C", "SQL_Criteria", "No_Criteria"),
            ("Result", "%s_Managers" % table.name),
        ],
        section="Manager: %s" % table.name,
        returns="%s_Managers" % table.name,
    )


def from_cache_params(schema, table, with_self=""):
    """Return the parameters passed to the Hash_%row function"""
    if with_self:
        tmp = ", ".join(
            [
                "%s.%s" % (with_self, schema.subprogram_name_from_field(p))
                for p in table.pk
            ]
        )
    else:
        tmp = ", ".join(["%s" % p.name for p in table.pk])
    return tmp


def from_cache_hash(schema, table, with_self=""):
    if table.has_cache:
        return "(%d, %s)" % (
            table.base_key,
            from_cache_params(schema, table, with_self),
        )
    else:
        return "(%d, No_Primary_Key)" % table.base_key


def detach(pretty, table, schema, translate):
    """Generate Detach subprograms
    TABLE is an instance of Table
    """

    # We return a Detached_*'Class, rather than a Detached_*_Access for the
    # following reason: returning an access type would be tempting for the user
    # to store directly in a persistent data structure. But the pointer really
    # belongs to the session, that will free it when no longer needed, so there
    # is a risk of Storage_Error.
    # Instead, we return the 'Class. A Detached_* is supposed to be a smart
    # pointer anyway, so any modification to it will affect all objects that
    # represent the same element.
    # This choice makes it slightly less convenient to manipulate the result of
    # Detach since basically we need a declare block.
    #
    # In a Detached_*, we store new pointer types, not the ones stored in the
    # session, since the latter might have a shorter lifetime.

    if table.pk != [] and table.has_cache:
        tr = {
            "row": translate["row"],
            "fromcache": from_cache_params(schema, table, with_self="Self"),
        }

        body = (
            "return Detached_%(row)s'Class (Session.From_Cache ("
            + "%(fromcache)s, No_Detached_%(row)s));"
        ) % {
            "row": translate["row"],
            "fromcache": from_cache_hash(schema, table, with_self=""),
        }

        pretty.add_subprogram(
            name="from_cache",
            params=[("session", "Session_Type")] + schema.params_get_pk(table),
            section="Elements: %(cap)s" % translate,
            returns="Detached_%(row)s'Class" % translate,
            comment="""Check whether there is already an element
with this primary key. If not, the returned value will be a null element
(test with Is_Null)""",
            body=body,
        )

        local = [
            (
                "R",
                "constant Detached_%(row)s'Class" % translate,
                "From_Cache (Self.Data.Session, %s)"
                % from_cache_params(schema, table, with_self="Self"),
            )
        ]
        body = """if R.Is_Null then
              return Detach_No_Lookup (Self, Self.Data.Session);
          else
              return R;
          end if;"""

    else:
        local = []
        body = "return Detach_No_Lookup (Self, Self.Data.Session);"
        tr = {}

    pretty.add_subprogram(
        name="detach",
        params=[("self", "%(row)s'Class" % translate)],
        section="Elements: %(cap)s" % translate,
        returns="Detached_%(row)s'Class" % translate,
        local_vars=local,
        body=body % tr,
    )

    # Internal version of Detach

    long = max_length([f.name for f in table.fields])
    aggregate = []
    decl = [
        ("Default", "Detached_%(row)s" % translate),
        (
            "Result",
            "Detached_%(row)s'Class" % translate,
            "Detached_%(row)s'Class (Session.Factory (Self, Default))" % translate,
        ),
    ]
    setters = ""

    for f in table.fields:
        if f.show:
            aggregate.append(f.free_field("Tmp"))
            conv = f.field_from_db("Self")
            aggregate.append("Tmp.ORM_%-*s := %s;" % (long + 3, f.name, conv))

    tests = []

    # Only keep foreign keys for tables for which we generate orm.* packages
    fk = [f for f in table.fk if f.foreign.show]

    for index, f in enumerate(fk):
        subp = subprogram_from_fk(f)
        ref = f.foreign.name
        d = {
            "name": subp,
            "ref": ref,
            "cap": subp.title(),
            "pkg": pkg_name,
            "table": table.name,
            "index": index,
            "row": f.foreign.row,
        }

        if f.show():
            decl.append(("FK_" + subp, "Detached_%(row)s_Access" % d))
            aggregate.append("Tmp.ORM_FK_%-*s := FK_%s;" % (long, subp, subp))

        # Have to create new access type, see above
        str = """FK_%(name)s := new Detached_%(row)s'Class'(
  I_%(ref)s.Internal_Element
  (Self, Upto_%(table)s_%(index)d (Self.Depth, LJ)).Detach);"""

        if f.show():
            if f.can_be_null():
                tests.append("         if LJ then\n" + str % d + "\n         end if;\n")
            else:
                tests.append(str % d)

    if tests:
        tests = "if Self.Depth > 0 then\n%s\n      end if;" % "\n".join(tests)
    else:
        tests = ""

    if table.fk:
        decl.append(("LJ", "constant Boolean", "Self.Data.Follow_LJ"))

    tests += setters

    pretty.add_subprogram(
        name="detach_no_lookup",
        comment="Same as Detach, but does not check the session cache",
        section="body",
        params=[
            ("self", "%(row)s'Class" % translate),
            ("session", database_connection),
        ],
        returns="Detached_%(row)s'Class" % translate,
        local_vars=decl + [("Tmp", "%(row)s_Data" % translate)],
        body="""
  if Result.Is_Null then
     Result.Set (%(row)s_DDR'
        (Detached_Data with Field_Count => %(field_count)s, others => <>));
  end if;

  Tmp := %(row)s_Data (Result.Unchecked_Get);
  %(tests)s %(aggregate)s
  %(traces)sSession.Persist (Result);
  return Result;
"""
        % {
            "tests": tests,
            "row": translate["row"],
            "field_count": translate["field_count"],
            "traces": debug_msg(table, "Creating", "Result.all"),
            "aggregate": "\n            ".join(sorted(aggregate)),
        },
    )


##########################
# Prepare the sections in the specs
# so that they appear in the right order
##########################


def order_sections(schema, pretty, all_tables):
    for table in list(all_tables.values()):
        if table.is_abstract:
            pretty.add_section(
                "Elements: %s" % table.name,
                "",
                "--  Interfaces corresponding to abstract tables" + " in the schema",
            )

    # First the elements declarations grouped, since their primitive ops
    # might reference each other

    pretty.add_section(
        "Types",
        "",
        """
    --  Detached_* elements extract the value from the list and store them
    --  locally. As a result, they remain valid even if the list is modified,
    --  but require more memory to store.
    --
    --  Other elements are only valid while the list from which they are
    --  created is not modified(see Element below). As soon as you iterate the
    --  list this element becomes invalid.
    --
    --  Direct lists are stored in memory, and can be traversed in any order.
    --  Forward lists can only be iterated forward. With some database backends
    --  this is much more efficient since only the current element needs to be
    --  stored in memory(and retrieved from the server).""",
    )

    # Then all their primitive operations, before we freeze the types

    for table in list(all_tables.values()):
        pretty.add_section("Elements: %s" % table.name, "")

    # Then instanciate the generic packages, which freezes the element types.
    # We can then define the manager types

    pretty.add_section("Managers(implementation details)", "")
    pretty.add_section("Manager types", "")

    # And now we can create the primitive ops for the managers and lists,
    # which might reference each other

    for table in list(all_tables.values()):
        if not table.is_abstract:
            pretty.add_section("Manager: %s" % table.name, "")

    # Finally we can declare the maanger objects, which freeze the manager
    # types.

    pretty.add_section("Managers", "")


#########################
# debug trace
#########################


def debug_msg(table, msg, self="Self"):
    if debug:
        return 'Debug_%s(%s, "%s");' % (table.row, self, msg)
    return ""


def add_debug_msg(pretty, table):
    if debug and not table.is_abstract:
        tmp = "& ".join(["Self.Data.%s'Img" % p.name for p in table.pk])
        if tmp:
            tmp = "& " + tmp
        body = """Trace(Me, Msg
           & "(%s, pk=" %s & ", "
           & System.Address_Image(Self.Data.all'Address)
           & ")");"""
        pretty.add_subprogram(
            name="Debug_%s" % table.row,
            params=[("Self", "Detached_%s'Class" % table.row), ("msg", "String", '""')],
            body=body,
            section="debug",
        )


##########################
# generate_orb_one_table
##########################


def generate_orb_one_table(name, schema, pretty, all_tables):
    """Generate Ada glue code for a specific table.
    out is an instance of file, writable"""

    table = schema.details[name]  # instance of dbgraph.Table

    add_debug_msg(pretty, table)

    schema.withs_for(name)

    tagged = ""
    if table.superClass:
        tagged = " and %s" % table.superClass.row

    translate = {
        "cap": table.name,
        "row": table.row,
        "pkg_name": pkg_name,
        "detached_data_fields": schema.detached_data_fields(table),
        "tagged": tagged,
        "field_count": len(table.fields) + len(table.fk),
        "free_fields": schema.free_fields(table),
        "equal": "\n         and ".join(schema.equal(table)),
    }

    if table.is_abstract:
        pretty.add_section(
            "Elements: %s" % table.name, """type %(row)s is interface;""" % translate
        )

    else:
        pretty.add_section(
            "Types",
            """

   type %(row)s is new Orm_Element%(tagged)s with null record;
   type %(row)s_DDR is new Detached_Data (%(field_count)s) with private;
   type Detached_%(row)s is  --  Get() returns a %(row)s_DDR
      new Sessions.Detached_Element%(tagged)s with private;
   type Detached_%(row)s_Access is access all Detached_%(row)s'Class;
   No_Detached_%(row)s : constant Detached_%(row)s;
   No_%(row)s : constant %(row)s;"""
            % translate,
        )

        pretty.add_section(
            "Manager types",
            """

   type I_%(cap)s_Managers is abstract new Manager with null record;
   package I_%(cap)s is new Generic_Managers
    (I_%(cap)s_Managers, %(row)s, Related_Depth, DBA.%(cap)s,
      Internal_Query_%(cap)s);
   type %(cap)s_Managers is new I_%(cap)s.Manager with null record;
   subtype %(cap)s_Stmt is I_%(cap)s.ORM_Prepared_Statement;

   subtype %(row)s_List is I_%(cap)s.List;
   subtype Direct_%(row)s_List is I_%(cap)s.Direct_List;
   Empty_%(row)s_List : constant %(row)s_List := I_%(cap)s.Empty_List;
   Empty_Direct_%(row)s_List : constant Direct_%(row)s_List :=
     I_%(cap)s.Empty_Direct_List;
 """
            % translate,
        )

        pretty.add_section(
            "Managers",
            """
   All_%(cap)s : constant %(cap)s_Managers :=
     (I_%(cap)s.All_Managers with null record);
"""
            % translate,
        )

        detach(pretty, table, schema, translate)
        internal_query(pretty, table, schema)
        create(pretty, table, schema)

        pretty.add_constants(
            [
                ("F_%s_%s" % (name, f.name), "constant", "%d" % index)
                for index, f in enumerate(table.fields)
                if f.show
            ]
        )
        pretty.add_unique_constants(
            [
                (
                    """procedure Unchecked_Free is new Ada.Unchecked_Deallocation
      ( %(row)s_DDR, %(row)s_Data)"""
                    % translate,
                )
            ]
            + schema.unchecked_free(name, all_tables)
        )

        # Constants Counts_%s are the number of fields needed for %s and its
        # FK at each depth

        if table.revert_fk:
            counts = table.fields_count_array(False, max_depth=max_depth)
            lj_counts = table.fields_count_array(True, max_depth=max_depth)
            counts = ["(%d,%d)" % (counts[c], lj_counts[c]) for c in range(len(counts))]
            pretty.add_constants(
                [
                    (
                        "Counts_%s" % table.name,
                        "constant Counts",
                        "(%s)" % ",".join(counts),
                    )
                ]
            )

        # Constants Upto_%s_%s is the index of the first fields for the second
        # %s when retrieving the fields for the first %s

        for index, fk in enumerate(table.fk):
            counts = table.fields_count_array(False, max_depth, fk)
            lj_counts = table.fields_count_array(True, max_depth, fk)
            counts = ["(%d,%d)" % (counts[c], lj_counts[c]) for c in range(len(counts))]
            pretty.add_constants(
                [
                    (
                        "Upto_%s_%d" % (table.name, index),
                        "constant Counts",
                        "(%s)" % ",".join(counts),
                    )
                ]
            )

        aliases = table.compute_table_aliases(max_depth=max_depth)

        if len(aliases) == 1:
            aliases = "0 => %d" % aliases[0]
        else:
            aliases = ",".join([str(a) for a in aliases])

        pretty.add_constants(
            [("Alias_%s" % table.name, "constant Alias_Array", "(%s)" % aliases)]
        )

        if table.pk:
            when_not_in_cache = (
                """
               declare
                 M : %(name)s_Managers := Filter
                   (All_%(cap)s,
                    """
                % {"name": table.name, "cap": pretty._title(name)}
                + ",".join(schema.call_create_params(name))
                + """);
                 L : I_%(cap)s.List;
               begin
                 M.Select_Related
                   (Depth, Follow_Left_Join => Follow_Left_Join);
                 M.Limit (1);
                 L := M.Get(Session);
                 if not L.Has_Row then
                    return No_Detached_%(row)s;
                 else
                    declare
                       E : constant %(row)s := L.Element;
                    begin
                       --  Workaround bug in gnat which is missing a call
                       --  to Finalize if we do not reset the list (K321-012)
                       L := I_%(cap)s.Empty_List;
                       return E.Detach_No_Lookup (Session);
                    end;
                 end if;
               end;"""
                % translate
            )

            if table.has_cache:
                local = [
                    (
                        "R",
                        "constant Detached_%s'Class" % table.row,
                        "From_Cache (Session, %s)"
                        % from_cache_params(schema, table, with_self=""),
                    )
                ]
                body = (
                    """if not R.Is_Null then return R; else """
                    + when_not_in_cache
                    + """end if;"""
                )
            else:
                local = []
                body = when_not_in_cache

            pretty.add_subprogram(
                name="get_%(row)s" % translate,
                params=[("Session", database_connection)]
                + schema.params_get_pk(table)
                + [
                    ("Depth", "Related_Depth", "0"),
                    ("Follow_Left_Join", "Boolean", "False"),
                ],
                local_vars=local,
                body=body,
                returns="detached_%(row)s'Class" % translate,
                section="Manager: %(cap)s" % translate,
            )

        if table.pk:
            pretty.add_subprogram(
                name='"="',
                params=[("op1", "%(row)s" % translate), ("op2", "%(row)s" % translate)],
                body="return %(equal)s;" % translate,
                returns="boolean",
                section="Elements: %(cap)s" % translate,
            )

            pretty.add_subprogram(
                name='"="',
                params=[
                    ("op1", "Detached_%(row)s" % translate),
                    ("op2", "Detached_%(row)s" % translate),
                ],
                body="""if Op1.Is_Null then return Op2.Is_Null;
                elsif Op2.Is_Null then return False; else
                return %(equal)s; end if;"""
                % translate,
                section="Elements: %(cap)s" % translate,
                returns="boolean",
                comment="""
                   Compares two elements using only the primary keys. All other
                   fields are ignored""",
            )

        pretty.add_subprogram(
            name="new_%(row)s" % translate,
            returns="Detached_%(row)s'Class" % translate,
            section="Elements: %(cap)s" % translate,
            local_vars=[
                ("Result", "Detached_%(row)s" % translate),
                ("Data", "%(row)s_DDR" % translate),
            ],
            body="Result.Set (Data); return Result;",
            comment="""
            Create a new element, but no attribute is set. Use Set_* to
            modify any attribute for which you need a value""",
        )

        has_pk = []
        where = []
        assign = []
        getpk = ""
        execute = "R.Fetch"

        for index, f in enumerate(table.fields):
            if f.is_pk():
                has_pk.append("D.ORM_%s = %s" % (f.name, f.default_for_field()))
                where.append("DBA.%s.%s = %s" % (table.name, f.name, f.to_return("D")))
                if getpk != "":
                    getpk = "null; --  Can't retrieve multi-key PK"
                    execute = "Execute"
                elif f.type.sql_type != "integer":
                    getpk = "null;  --  Can only retrieve integer PK"
                    execute = "Execute"
                else:
                    getpk = (
                        "D.ORM_%s := R.Last_Id (Self.Session.DB," + " DBA.%s.%s);"
                    ) % (f.name, table.name, f.name)

            elif f.is_fk():
                tr = {
                    "index": index + 1,
                    "table": table.name,
                    "row": translate["row"],
                    "name": f.name,
                    "default": f.default_for_field(),
                    "fk": f.fk.name,
                    "fkrow": f.fk.table.row,
                    "fkall": f.fk.to_return("D2"),
                    "value": f.to_return("D"),
                }

                if translate["row"] == f.fk.table.row:
                    self_check = (
                        """
                      if Detached_%(row)s'Class (Self) =
                         D.ORM_FK_%(name)s.all
                      then
                         raise Self_Referencing with
                            "%(row)s is self referencing";
                      end if;
                      """
                        % tr
                    )
                else:
                    self_check = ""

                tr["self_check"] = self_check

                if f.fk.table.show:
                    assign.append(
                        """if Mask (%(index)d) then
                        if D.ORM_%(name)s /= %(default)s then
                          A := A & (DBA.%(table)s.%(name)s = %(value)s);
                        else
                           %(self_check)s
                           declare
                              D2 : constant %(fkrow)s_Data :=
                              %(fkrow)s_data (D.ORM_FK_%(name)s.Unchecked_Get);
                           begin
                              if D2.ORM_%(fk)s = %(default)s then
                                 Self.Session.Insert_Or_Update
                                    (D.ORM_FK_%(name)s.all);
                              end if;

                              A := A & (DBA.%(table)s.%(name)s = %(fkall)s);
                           end;
                        end if;
                     end if;"""
                        % tr
                    )
                else:
                    assign.append(
                        """if Mask (%(index)d) then
                          A := A & (DBA.%(table)s.%(name)s = %(value)s);
                     end if;"""
                        % tr
                    )

            else:
                assign.append(
                    """if Mask (%d) then
                      A := A & (DBA.%s.%s = %s);
                    end if;"""
                    % (index + 1, table.name, f.name, f.to_return("D"))
                )

        tr = {
            "table": table.name,
            "setassign": "\n".join(assign),
            "getpk": getpk,
            "execute": execute,
            "where": " AND ".join(where),
        }

        local_vars = [
            (
                "D",
                "constant %(row)s_Data" % translate,
                "%(row)s_Data (Self.Unchecked_Get)" % translate,
            ),
            ("Q", "SQL_Query"),
            ("A", "SQL_Assignment", "No_Assignment"),
        ]

        if has_pk:
            local_vars.append(
                ("Missing_PK", "constant Boolean", " or else ".join(has_pk))
            )

        if execute == "R.Fetch":
            local_vars.append(("R", "Forward_Cursor"))

        if has_pk:
            insert_or_update = (
                """
                %(setassign)s
                if Missing_PK then
                   Q := SQL_Insert (A);
                else
                   Q := SQL_Update (DBA.%(table)s, A, %(where)s);
                end if;
                %(execute)s (Self.Session.DB, Q);

                if Missing_PK and then Success (Self.Session.DB) then
                   PK_Modified := True;
                   %(getpk)s
                end if;
                """
                % tr
            )
        else:
            insert_or_update = (
                """
                %(setassign)s
                Q := SQL_Insert (A);
                %(execute)s (Self.Session.DB, Q);"""
                % tr
            )

        pretty.add_subprogram(
            name="insert_or_update",
            params=[
                ("self", "in out detached_%(row)s" % translate),
                ("PK_Modified", "in out Boolean"),
                ("Mask", "Dirty_Mask"),
            ],
            overriding=True,
            section="internal",
            local_vars=local_vars,
            body=insert_or_update,
        )

        if has_pk:
            local_vars = [
                (
                    "D",
                    "constant %(row)s_Data" % translate,
                    "%(row)s_Data (Self.Unchecked_Get)" % translate,
                )
            ]
            delete_body = (
                "Execute (Self.Session.DB," + " SQL_Delete (DBA.%(table)s, %(where)s));"
            ) % tr
        else:
            local_vars = []
            delete_body = (
                "raise Program_Error with" + ' "Table %(cap)s has no primary key";'
            ) % translate

        pretty.add_subprogram(
            name="internal_delete",
            params=[("self", "detached_%(row)s" % translate)],
            overriding=True,
            section="internal",
            local_vars=local_vars,
            body=delete_body,
        )

        on_add = ""
        for f in table.fk:
            if f.foreign.show:
                name = subprogram_from_fk(f)
                on_add += """if D.ORM_FK_%s /= null then
                   Self.Session.Persist (D.ORM_FK_%s.all);
                   end if;""" % (
                    name,
                    name,
                )
        if on_add:
            pretty.add_subprogram(
                name="on_persist",
                overriding=True,
                params=[("self", "Detached_%(row)s" % translate)],
                local_vars=[
                    (
                        "D",
                        "constant %(row)s_Data" % translate,
                        "%(row)s_Data (Self.Unchecked_Get)" % translate,
                    )
                ],
                section="internal",
                body="if Persist_Cascade (Self.Session) then " + on_add + " end if;",
            )

        translate["traces"] = debug_msg(table, "Free")
        pretty.add_subprogram(
            name="free",
            params=[("self", "in out %(row)s_DDR" % translate)],
            section="internal",
            overriding=True,
            body="""%(free_fields)s Free (Detached_Data (Self));""" % translate,
        )

        if not table.is_abstract:
            if table.has_cache:
                unset = " or else ".join(
                    [
                        "Self.ORM_%s = %s" % (p.name, p.type.default_record)
                        for p in table.pk
                    ]
                )
                tmp = ", ".join([p.to_return("Self") for p in table.pk])
                body = (
                    "if %s then" % unset
                    + "  return (%d, No_Primary_Key);" % (table.base_key,)
                    + " else "
                    + "  return (%d, %s);" % (table.base_key, tmp)
                    + " end if;"
                )

            else:
                body = (
                    """
  --  Not cachable, since the PK is not a single integer field
  return (%s, No_Primary_Key);"""
                    % table.base_key
                )

            pretty.add_subprogram(
                name="key",
                params=[("self", "%(row)s_DDR" % translate)],
                returns="Element_Key",
                section="internal",
                overriding=True,
                body=body,
            )

    # Prepare the getters
    # Prepare the setters for simple values. These are only available for
    # the Detached_* types, since we need a place where to store the new
    # values

    for index, f in enumerate(table.fields):
        if f.show:
            pretty.add_subprogram(
                name=schema.subprogram_name_from_field(f),
                params=[("self", "%(row)s" % translate)],
                returns=f.type.ada_return,
                section="Elements: %(cap)s" % translate,
                abstract=table.is_abstract,
                comment=f.comment,
                body="return %s;" % f.ada_from_db(cursor="Self"),
            )

            if not table.is_abstract:
                getter = "return %s;" % f.to_return(
                    "%s_Data (Self.Unchecked_Get)" % f.table.row
                )

                if f.is_pk():
                    # We must not change primary keys
                    setter = None
                    local = []
                else:
                    free = f.free_field("D")
                    if f.is_fk() and f.fk.table.show:
                        free += "Unchecked_Free (D.ORM_FK_%s);" % f.name

                    local = [
                        (
                            "D",
                            "constant %(row)s_Data" % translate,
                            "%(row)s_Data (Self.Unchecked_Get)" % translate,
                        )
                    ]
                    setter = """%sD.ORM_%s := %s;
                    Self.Set_Modified (%d);
                    """ % (
                        free,
                        f.name,
                        f.to_field("Value"),
                        index + 1,
                    )

                pretty.add_property(
                    schema=schema,
                    row="%(row)s" % translate,
                    field=f,
                    getter=getter,
                    setter=setter,
                    setter_local_vars=local,
                    type=f.type.ada_return,
                    abstract=table.is_abstract,
                    section="Elements: %(cap)s" % translate,
                )

    # Prepare the getters for foreign keys

    for index, fk_field in enumerate(table.fk):
        table_name = subprogram_from_fk(fk_field)

        # Do not generate code if we do not generate code for the external
        # table
        if fk_field.show():
            params = []
            detached_params = []
            reset_fk = []
            is_same = []
            free_fk = []

            for ffrom, fto in fk_field.pairs:
                params.append(
                    "%s => Self.%s"
                    % (fto.name, schema.subprogram_name_from_field(ffrom))
                )
                detached_params.append("%s => %s" % (fto.name, ffrom.to_return("D")))
                reset_fk.append(
                    "D.ORM_%s := %s;"
                    % (
                        ffrom.name,
                        fto.to_field(
                            "Value.%s" % schema.subprogram_name_from_field(fto)
                        ),
                    )
                )
                free_fk.append(ffrom.free_field("D"))
                is_same.append("%s = %s" % (fto.to_return("D"), ffrom.to_return("D")))

            body = """if Current (Self.Current) /= Self.Index then
               raise Cursor_Has_Moved;
            end if;

            if Self.Depth > 0"""

            if fk_field.can_be_null():
                body += " and then Self.Data.Follow_LJ"

            body += """ then
               return I_%(ref)s.Internal_Element
                 (Self,
                  Upto_%(cap)s_%(index)d (Self.Depth, Self.Data.Follow_LJ));
            else
               if not Dynamic_Fetching then
                  raise Field_Not_Available with
                     "Dynamic fetching disabled for %(name)s";
               end if;

               return Filter (All_%(ref)s, %(pk)s)
                  .Limit (1).Get (Self.Data.Session).Element;
            end if;
 """ % {
                "name": table_name,
                "index": index,
                "ref": fk_field.foreign.name,
                "cap": table.name,
                "pk": ",".join(params),
            }

            pretty.add_subprogram(
                name=schema.subprogram_name_from_field(table_name),
                params=[("self", "%(row)s" % translate)],
                returns="%s'Class" % fk_field.foreign.row,
                abstract=table.is_abstract,
                section="Elements: %(cap)s" % translate,
                body=body,
            )

            if not table.is_abstract:
                tr = {
                    "pkg_name": pkg_name,
                    "name": table_name,
                    "row": fk_field.foreign.row,
                    "is_same": " and ".join(is_same),
                    "pk": ",".join(detached_params),
                }

                if store_connections:
                    params = []
                else:
                    params = [("Session", database_connection)]

                getter = "if D.ORM_FK_%(name)s = null then " % tr

                if tr["row"] == translate["row"]:
                    getter += (
                        """
                     if %(is_same)s then
                        --  ??? Avoid reference cycle. Perhaps we could simply
                        --  avoid the cache for all foreign keys, and only
                        --  rely on the session cache for the elements instead
                        --  ??? Or use a weak reference
                        return Detached_%(row)s'Class (Self);
                     end if;
                     """
                        % tr
                    )

                getter += (
                    """if not Dynamic_Fetching then
                     raise Field_Not_Available with
                        "Dynamic fetching disabled for %(name)s";
                  end if;"""
                    % tr
                )

                if store_connections:
                    getter += (
                        """S := Session (Self);
                      if S = No_Session then
                          raise Field_Not_Available with
                              "Element is detached from any session";
                      end if;
                      D.ORM_FK_%(name)s := new Detached_%(row)s'Class'
                         (Get_%(row)s (S, %(pk)s));
                      """
                        % tr
                    )

                else:
                    getter += (
                        """
                       D.ORM_FK_%(name)s := new Detached_%(row)s'Class'
                       (Get_%(row)s (Session, %(pk)s));"""
                        % tr
                    )

                getter += """end if; return D.ORM_FK_%(name)s.all;""" % tr

                setlocal = [
                    (
                        "D",
                        "constant %(row)s_Data" % translate,
                        "%(row)s_Data (Self.Unchecked_Get)" % translate,
                    )
                ]
                free = (
                    ffrom.free_field("D")
                    + "".join(free_fk)
                    + "Unchecked_Free (D.ORM_FK_%s);" % table_name
                    + "\n".join(reset_fk)
                )
                setter = ""
                for idx, f2 in enumerate(table.fields):
                    if f2 == ffrom:
                        setter = """%sD.ORM_FK_%s := new Detached_%s'Class'(Value);

                    Self.Set_Modified (%d);
                    if Persist_Cascade (Self.Session) then
                       Self.Session.Persist (D.ORM_FK_%s.all);
                    end if;
                    """ % (
                            free,
                            table_name,
                            tr["row"],
                            idx + 1,
                            table_name,
                        )

                pretty.add_property(
                    schema=schema,
                    row="%(row)s" % translate,
                    field=table_name,
                    getter=getter,
                    setter=setter,
                    setter_local_vars=setlocal,
                    getter_local_vars=[
                        (
                            "D",
                            "constant %(row)s_Data" % translate,
                            "%(row)s_Data (Self.Unchecked_Get)" % translate,
                        ),
                        ("S", "Session_Type"),
                    ],
                    type="Detached_%s'Class" % fk_field.foreign.row,
                    abstract=table.is_abstract,
                    section="Elements: %(cap)s" % translate,
                )

    #                if not table.is_abstract:
    #                    # From a table's manager, get a manager for the tables
    #                    # that are related via a FK.
    #
    #                    pretty.add_subprogram(
    #                       name=schema.subprogram_name_from_field(table_name),
    #                       params=[("self",
    #                                "I_%(row)s_Managers'Class" % translate)],
    #                       returns="%s_Managers" % fk_field.foreign.row,
    #                       section="Manager: %(cap)s" % translate,
    #                       local_vars=[("Q", "constant SQL_Query",
    #                                      "I_%s.Build_Query(Self, +DBA.%s.%s)"
    #                                    % (table.name,
    #                                       table.name,
    #                                       table_name))],
    #                       body="""return All_%s.Filter
    #        (SQL_In(DBA.%s.%s, Q));""" % (
    #                          fk_field.foreign.name,
    #                          fk_field.foreign.name,
    #                          fk.pairs[0][1].name))

    # Generate revert-FK getters for elements

    for fk in table.revert_fk:  # fk.foreign is always table
        foreign = fk.pairs[0][0].table  # revert the FK relationship
        if fk.revert and fk.foreign.show and foreign.show:
            pretty.add_subprogram(
                name=schema.subprogram_name_from_field(fk.revert),
                params=[("self", "%(row)s'Class" % translate)],
                returns="%s_Managers" % foreign.name,
                section="Manager: %(cap)s" % translate,
                abstract=table.is_abstract,
                body="return Filter (All_%s, %s => Self.%s);"
                % (foreign.name, fk.pairs[0][0].name, fk.pairs[0][1].name),
            )

            if not table.is_abstract:
                pretty.add_subprogram(
                    name=schema.subprogram_name_from_field(fk.revert),
                    params=[("self", "Detached_%(row)s'Class" % translate)],
                    returns="%s_Managers" % foreign.name,
                    section="Manager: %(cap)s" % translate,
                    abstract=table.is_abstract,
                    body="return Filter (All_%s, %s => Self.%s);"
                    % (foreign.name, fk.pairs[0][0].name, fk.pairs[0][1].name),
                )

            pretty.add_subprogram(
                name=schema.subprogram_name_from_field(fk.revert),
                params=[("self", "I_%(cap)s_Managers'Class" % translate)],
                returns="%s_Managers" % foreign.name,
                section="Manager: %(cap)s" % translate,
                abstract=table.is_abstract,
                local_vars=[
                    (
                        "Q",
                        "constant SQL_Query",
                        "I_%s.Build_Query(Self, +DBA.%s.%s)"
                        % (table.name, table.name, fk.pairs[0][1].name),
                    )
                ],
                body="""return All_%s.Filter
        (SQL_In(DBA.%s.%s, Q));"""
                % (foreign.name, foreign.name, fk.pairs[0][0].name),
            )

    if not table.is_abstract:
        pretty.add_private_before(
            """
    type %(row)s_DDR is new Detached_Data (%(field_count)s) with record
%(detached_data_fields)s;
    end record;
    type %(row)s_Data is access all %(row)s_DDR;
    """
            % translate
        )
        pretty.add_private_after(
            """
    type Detached_%(row)s
       is new Sessions.Detached_Element%(tagged)s with null record;
    No_%(row)s : constant %(row)s :=(No_Orm_Element with null record);
    No_Detached_%(row)s : constant Detached_%(row)s :=
      (Sessions.Detached_Element with null record);
 """
            % translate
        )


#################
# generate_orm
#################


def generate_orm(setup, pkg_name, tables=None, omit=None, out=sys.stdout):
    """Generate ORB packages for a set of tables.
    SETUP is an object that contains the database description(.db_name,...)
    Do not generate data for the fields in OMIT("table.field").
    """

    if tables is None:
        tables = []
    if omit is None:
        omit = []

    pretty = Pretty_Printer(
        out,
        casing=[
            "Follow_LJ",
            "GNAT",
            "GNATCOLL",
            "SQL",
            "SQL_Table_List",
            "SQL_Query",
            "SQL_Field_List",
            "SQL_Field",
        ],
    )
    schema = Schema(setup, tables, pretty, all_tables=tables, omit=omit)
    if not tables:
        tables = schema.details
    else:
        t = {}
        for p in tables:
            t[p] = schema.details[p.lower()]
        tables = t

    pretty.start_package(pkg_name)

    # First generate abstract tables, since other fields will inherit from
    # them

    order_sections(schema, pretty, tables)

    table_image_body = "case Table is"

    for t in sorted(tables):
        table_image_body += ' when %d => return "%s";' % (
            tables[t].base_key,
            tables[t].name,
        )
        if tables[t].is_abstract:
            generate_orb_one_table(t, schema, pretty, tables)

    for t in sorted(tables):
        if not tables[t].is_abstract:
            generate_orb_one_table(t, schema, pretty, tables)

    table_image_body += " when others => return Table'Image; end case;"
    # pretty.add_subprogram(
    #    name="Image",
    #    params=[("Table", "Integer")],
    #    returns="String",
    #    body=table_image_body,
    #    section="Internal")

    pretty.terminate_package(need_dba=True)


class DBSetup(object):
    """This class describes how to get the schema from a database. This
    schema can either be read from a file, or from a live database. This
    class abstracts those accesses.
    """

    @staticmethod
    def from_file(filename):
        result = DBSetup()
        result.db_model = os.path.abspath(filename)
        return result

    @staticmethod
    def from_live_db(
        dbname, dbuser, dbhost="localhost", dbpassword="", dbtype="sqlite"
    ):
        result = DBSetup()
        result.db_name = dbname
        result.db_type = dbtype
        result.db_user = dbuser
        result.db_host = dbhost
        result.db_password = dbpassword

    def gnatcoll_db2ada(self, args=None):
        """Executes gnatcoll_db2ada, passing the required args to access
        the database model, and extra arguments if needed.
        """

        if args is None:
            args = []

        if hasattr(self, "db_model"):
            p = exec_or_fail(
                ["gnatcoll_db2ada", "-dbmodel", self.db_model] + args,
                stdout=subprocess.PIPE,
            )

        else:
            p = exec_or_fail(
                [
                    "gnatcoll_db2ada",
                    "-dbhost",
                    self.db_host,
                    "-dbname",
                    self.db_name,
                    "dbtype",
                    self.db_type,
                    "-dbuser",
                    self.db_user,
                    "-dbpasswd",
                    self.db_password,
                ]
                + args,
                stdout=subprocess.PIPE,
            )

        return p.stdout

    def get_schema(self):
        """Returns a textual description of the schema of the database"""
        if hasattr(self, "db_model"):
            return open(self.db_model).read()
        else:
            return self.gnatcoll_db2ada(["-text"]).read()


@save_dir
def create_orm(setup, pkg_name, indir, tables=None, omit=None):
    """Creates INDIR/orm-* based on the database defined in the config.
    First remove any orm-*.ad? in that directory
    TABLES can be used to limit the generation to a subset of the tables.
    OMIT is used to remove some of the tables from the set.
    Return 1 in case of error, 0 for success (exit status for the shell)
    """

    if tables is None:
        tables = []
    if omit is None:
        omit = []

    for dirpath, _, filenames in os.walk(indir):
        for f in filenames:
            if os.path.splitext(f)[1] in (".ads", ".adb") and f.startswith("orm-"):
                unlink_if_exist(os.path.join(dirpath, f))

    os.chdir(indir)
    out = open("tmp_orm", "w")
    try:
        generate_orm(
            setup,
            pkg_name=pkg_name,
            tables=tables,
            omit=omit,  # omit circular deps
            out=out,
        )
        out.close()
        exec_or_fail(["gnatchop", "-q", "-w", "tmp_orm"])

    except Cannot_Parse_Schema:
        return 1

    unlink_if_exist("tmp_orm")
    return 0


class Field_Type(object):
    def __init__(
        self,
        sql_type,  # SQL type
        ada_return,  # As an Ada return value
        ada_param,  # As an Ada subprogram parameter
        default_param,  # default value for the Get parameter
        ada_field,  # As an Ada record field
        default_record,  # default value for the record
        value_from_db,  # Ada value from db (%1=Cursor, %2=index)
        to_return,  # convert from field type to return type
        free_field,  # How to free the field
        img,  # From a field type to a string
        to_field,
    ):  # Convert from ada_param to ada_field
        self.sql_type = sql_type
        self.ada_return = ada_return
        self.ada_param = ada_param
        self.default_param = default_param
        self.ada_field = ada_field
        self.default_record = default_record
        self._value_from_db = value_from_db
        self._to_return = to_return  # %1 => value to convert
        self._to_field = to_field  # %1 => entity to convert
        self._img = img  # %1 => value to convert
        self._free_field = free_field

    __all_types = None

    @staticmethod
    def get(sql):
        if not Field_Type.__all_types:
            Field_Type.__all_types = {
                # Map to Unbounded_String. This avoids having to do explicit
                # memory management, and in particular we can take advantage
                # of copy-on-write done for Unbounded_String rather than
                # redo it ourselves (with complex support for multi-tasking)
                "text": Field_Type(
                    "text",
                    "String",
                    "String",
                    "No_Update",
                    "Unbounded_String",
                    "Null_Unbounded_String",
                    "String_Value (%s, %s)",
                    "To_String (%s)",
                    "",
                    "%s",
                    "To_Unbounded_String (%s)",
                ),
                "integer": Field_Type(
                    "integer",
                    "Integer",
                    "Integer",
                    -1,
                    "Integer",
                    -1,
                    "Integer_Value (%s, %s)",
                    "%s",
                    "",
                    "%s'Img",
                    "%s",
                ),
                "bigint": Field_Type(
                    sql_type="bigint",
                    ada_return="Long_Long_Integer",
                    ada_param="Long_Long_Integer",
                    default_param=-1,
                    ada_field="Long_Long_Integer",
                    default_record=-1,
                    value_from_db="Bigint_Value (%s, %s)",
                    to_return="%s",
                    free_field="",
                    img="Long_Long_Integer'Image (%s)",
                    to_field="%s",
                ),
                "autoincrement": Field_Type(
                    "integer",
                    "Integer",
                    "Integer",
                    -1,
                    "Integer",
                    -1,
                    "Integer_Value (%s, %s)",
                    "%s",
                    "",
                    "%s'Img",
                    "%s",
                ),
                "time": Field_Type(
                    "time",
                    "Ada.Calendar.Time",
                    "Ada.Calendar.Time",
                    "No_Time",
                    "Ada.Calendar.Time",
                    "No_Time",
                    "Time_Value (%s, %s)",
                    "%s",
                    "",
                    "%s'Img",
                    "%s",
                ),
                "float": Field_Type(
                    "float",
                    "Float",
                    "Float",
                    "Float'First",
                    "Float",
                    "Float'First",
                    "Float_Value (%s, %s)",
                    "%s",
                    "",
                    "%s'Img",
                    "%s",
                ),
                "boolean": Field_Type(
                    "boolean",
                    "Boolean",
                    "TriBoolean",
                    "Indeterminate",
                    "Boolean",
                    "False",
                    "Boolean_Value (%s, %s)",
                    "%s",
                    "",
                    "%s'Img",
                    "%s",
                ),
                "money": Field_Type(
                    "money",
                    "GNATCOLL.SQL.T_Money",
                    "GNATCOLL.SQL.T_Money",
                    "GNATCOLL.SQL.T_Money'First",
                    "GNATCOLL.SQL.T_Money",
                    "GNATCOLL.SQL.T_Money'First",
                    "Money_Value (%s, %s)",
                    "%s",
                    "",
                    "%s'Img",
                    "%s",
                ),
            }

        sql = sql.lower()
        if sql in (
            "timestamp without time zone",
            "timestamp with time zone",
            "timestamp",
            "date",
        ):
            sql = "time"
        elif sql.startswith("character") or sql == "json":
            sql = "text"

        try:
            return Field_Type.__all_types[sql]
        except Exception:
            sys.stderr.write("Unsupported SQL type: %s\n" % sql)
            raise


class Field(object):
    """A field in a database table"""

    def __init__(self, table, name, type, default, comment, null, show):
        """SHOW is whether we should generate subprograms for it or not"""
        self.table = table  # Instance of Table
        self.name = name.title()  # Normalized name
        self.type = type  # string, will be replaced with Field_Type
        self.comment = comment
        self.null = null
        self.show = show

        self._fk = None
        self.pk = False  # This field is a primary key

        self.__default = default  # See default_field () instead
        if default.find("::") != -1:
            self.__default = default[: default.find("::")]  # Remove type casts

    def __repr__(self):
        return "Field<%s.%s>" % (self.table.name, self.name)

    @property
    def fk(self):
        """If SELF is a foreign key, return a pointer to the field in the
        foreign table
        """
        if self._fk is None:
            for f in self.table.fk:
                for p in f.pairs:
                    if self == p[0]:
                        self._fk = p[1]
                        break
        return self._fk

    def field_from_db(self, cursor):
        """Retrieves a field value by reading the current row from CURSOR
        and checking the specific TABLE.FIELD
        """
        return self.to_field(entity=self.ada_from_db(cursor))

    def ada_from_db(self, cursor):
        """Retrieves an Ada value by reading the current row from CURSOR"""
        return self.type._value_from_db % (
            cursor,
            "F_%s_%s" % (self.table.name, self.name),
        )

    def free_field(self, element):
        """Returns the string used to free ELEMENT.FIELD if needed"""
        if self.type._free_field:
            return self.type._free_field % ("%s.ORM_%s" % (element, self.name),) + ";"
        else:
            return ""

    def to_field(self, entity):
        """Converts ENTITY to the proper type to use in a record"""
        return self.type._to_field % entity

    def to_return(self, data):
        """Return the value stored in memory to the proper return type"""
        return self.type._to_return % ("%s.ORM_%s" % (data, self.name))

    def image(self, data=""):
        """Converts a value to a string, for display"""
        if data == "":
            return self.type._img % ("%s" % self.name)
        else:
            return self.type._img % ("%s.ORM_%s" % (data, self.name))

    def is_fk(self):
        """Whether SELF is a foreign key for its table, ie a field referencing
        another table
        """
        return self.fk is not None

    def is_pk(self):
        """Whether SELF is a primary key for its table"""
        for f in self.table.pk:
            if f == self:
                return True
        return False

    def default_for_field(self):
        """Return the default value to use for a record field"""
        if self.__default:
            if (
                self.__default.lower().find("now()") != -1
                or self.__default.lower() == "now"
            ):
                return "Clock"

            elif self.__default.lower().find("'now'") != -1:
                return "Clock"

            elif self.__default == "t":
                return "True"

            elif self.__default == "f":
                return "False"

            elif not self.__default.startswith("nextval"):
                if self.__default == "''":
                    return "Null_Unbounded_String"
                elif self.__default[0] == "'":
                    return 'To_Unbounded_String ("%s")' % self.__default[1:-1]
                else:
                    return self.__default.title()

        return self.type.default_record

    def resolve_fk(self, all_tables):
        """Lookup the type of field if it was unknown."""

        if self.type.startswith("FK "):
            # Look for the actual type, recursively since the PK of
            # the foreign table might itself be a FK
            pk = self.type
            while isinstance(pk, str) and pk.startswith("FK "):
                descr = Foreign_Key.parse_fk_descr(self, self.name, pk)
                if len(all_tables[descr.foreign_name].pk) > 1:
                    print(
                        (
                            "Error: '%s.%s' references '%s', which has more"
                            + " than one PK"
                        )
                        % (self.table.name, self.name, descr.foreign_name)
                    )
                pk = all_tables[descr.foreign_name].pk[0].type

            if isinstance(pk, str):
                self.type = Field_Type.get(pk)
            else:
                self.type = pk
        else:
            self.type = Field_Type.get(self.type)


class Foreign_Key(object):
    def __init__(self, from_name, foreign_name, pairs, revert):
        """FOREIGN_NAME is the name of the foreign table we are referencing
        PAIRS is a list of tuples (from, to), where from is the name of a
              field in FROM_NAME, and to is the name of a field in FOREIGN.
        """
        self.from_name = from_name  # A string, will be deleted
        self.foreign_name = foreign_name  # A string, will be deleted
        self.foreign = None  # A Table, will be resolved later
        self.pairs = pairs  # Initially  strings, but afterward Fields
        self.revert = revert  # Empty string if should be ignored

    def resolve_fk(self, all_tables):
        """Once we know all tables, complete the definition of foreign keys
        by pointing to the right table instances and completing the pairs
        """

        table = all_tables[self.from_name.lower()]
        self.foreign = all_tables[self.foreign_name.lower()]
        del self.foreign_name
        del self.from_name

        for index, p in enumerate(self.pairs):
            if p[1] is None:  # references the PK of the foreign table
                self.pairs[index] = (table.get_field(p[0]), self.foreign.pk[0])
            else:
                self.pairs[index] = (
                    table.get_field(p[0]),
                    self.foreign.get_field(p[1]),
                )
            if self.pairs[index][0] is None:
                print("Couldn't resolve field %s.%s" % (table, p[0]))

    def can_be_null(self):
        """Whether the foreign key can be NULL.
        If it must always be set, False is returned
        """
        for p in self.pairs:
            if p[0].null:
                return True
        return False

    def show(self):
        """Whether we should display this foreign key"""
        if not self.foreign.show:
            return False
        for p in self.pairs:
            if not p[0].show:
                return False
        return True

    @staticmethod
    def parse_fk_descr(table, field_name, descr):
        """Parse a description of a foreign key in the definition of TABLE.
        DESCR contains text like "FK table(revert)".
        """
        pairs = [(field_name, None)]
        revert = re.search(r"\((.*)\)", descr)
        if revert:
            return Foreign_Key(
                table.name, descr[3 : revert.start(1) - 1], pairs, revert.group(1)
            )
        else:
            return Foreign_Key(
                table.name, descr[3:], pairs, table.name + "_" + field_name + "_id"
            )


class Table(object):
    """Describes a database table"""

    base_key = 0

    def __init__(self, name, row, show=True, is_abstract=False, superClass=None):
        """FIELDS is a dictionary of Field, indexed by the name of the field
        PK is a list of Field that make up the primary key
        FK is a list of Foreign_Key
        ROW is the name of a row instance ("book" if table is "books")
        SHOW is true if we should do some output for self
        """
        self.name = name.title()  # string
        self.fields = []  # list of Field
        self.fk = []  # list of Foreign_Key
        self.pk = []  # list of Field
        self.row = row.title()  # string
        self.is_abstract = is_abstract
        self.show = show
        self.superClass = superClass  # a Str, then will be instance of Table
        self.revert_fk = []  # the FK from other tables that point to self

        self.base_key = Table.base_key
        Table.base_key += 1000000

        self.has_cache = True
        # Whether these items can be cached in the database. When they can't,
        # for instance because their PK is not a single integer field. In this
        # case, we generate a dummy Key function for the table, which always
        # indicate there is no PK, and the element will not be saved in the
        # session cache as a result. We also need to make sure that no other
        # part of the generated file uses From_Cache.

    def __repr__(self):
        return "Table<%s>" % self.name

    def fields_count(self, depth, follow_lj, until=""):
        """Remove the count of fields that in SELF and all its fk-related
        tables up to DEPTH. If FOLLOW_LF is False, ignore those fk that can
        be null.  Stops when we find a FK equal to UNTIL, if the latter is
        specified.
        """
        result = len(self.fields)
        if depth > 0:
            for fk in self.fk:
                if fk == until:
                    break
                if follow_lj or not fk.can_be_null():
                    result += fk.foreign.fields_count(depth - 1, follow_lj)
        return result

    def fields_count_array(self, follow_lj, max_depth, until=""):
        """Same as fields_count, but returns a list with all possible fields
        count at any depth. MAX_DEPTH is used to avoid infinite recursion
        (a table that references itself for instance)
        """
        result = []
        depth = 0
        while depth <= max_depth:
            result.append(self.fields_count(depth, follow_lj, until=until))
            depth += 1
        return result

    def get_field(self, name):
        """Retrieve field, by name"""
        name = name.lower()
        for f in self.fields:
            if f.name.lower() == name:
                return f
        return None

    def compute_table_aliases(self, max_depth):
        """See description of ORM.Alias_Array"""

        class Outscope:  # so that internal can modify alias
            pass

        Outscope.seen = set()  # tables already in the query
        Outscope.alias = 0  # current alias

        def internal(table, alias, start_index, depth):
            """Compute array for table TABLE, where START_INDEX is its first
            entry in the resulting array. ALIAS is the alias to use for
            TABLE
            """

            local = [alias]

            # First compute the aliases for all FK (breadth first search), so
            # that smaller numbers are used for small depth

            if depth < max_depth and table.fk:
                aliases = []
                for fk in table.fk:
                    if fk.foreign.name in Outscope.seen:
                        aliases.append(Outscope.alias)
                        Outscope.alias += 1
                    else:
                        aliases.append(-1)
                        Outscope.seen.add(fk.foreign.name)

                local += [-2] * len(table.fk)  # reserve space

                for index, fk in enumerate(table.fk):
                    local[index + 1] = start_index + len(local)  # Jump
                    local += internal(
                        fk.foreign, aliases[index], local[index + 1], depth + 1
                    )

            return local

        Outscope.seen.add(self.name)
        return internal(self, alias=-1, start_index=0, depth=0)

    def resolve_fk(self, all_tables):
        """Lookup the type of fields that were defined as "FK ..."
        ALL_TABLES must contain the set of all tables in the database.
        Return False in case of error.

        This is called immediately after parsing the database schema.
        """

        for f in self.fk:
            f.resolve_fk(all_tables)
            f.foreign.revert_fk.append(f)  # revert FK relationships

        for f in self.fields:
            f.resolve_fk(all_tables)

        if self.superClass:  # Add fields inherited from abstract parent
            self.superClass = all_tables[self.superClass]

            for f in self.superClass.fields:
                f = copy.copy(f)
                f.table = self
                self.fields.append(f)
                if f.pk:
                    self.pk.append(f)

        if len(self.pk) != 1:
            self.has_cache = False
        elif self.pk[0].type.ada_param != "Integer":
            self.has_cache = False


def get_db_schema(setup, requires_pk=False, all_tables=None, omit=None):
    """Parse the schema of the database (database access is described in
    setup, which is an instance of configfile.ConfigFile).
    If requires_pk is True, a warning is raised for all tables that have
    no PRIMARY KEY. Views never generate a warning.
    OMIT is the list of fields to omit (in the form "table.field").
    ALL_TABLES, if specified, lists the tables for which we do output.
    """

    if all_tables is None:
        all_tables = []
    if omit is None:
        omit = []

    schematxt = setup.get_schema()
    tables = {}  # Index is lower cases table name
    table = None  # Instance of Table

    for line in schematxt.splitlines():
        if line.startswith("|") and not line.startswith("|--"):
            fields = line.split("|")
            fields[1] = fields[1].strip()
            fields[2] = fields[2].strip()

            if (
                fields[1].startswith("VIEW")
                or fields[1].startswith("TABLE")
                or fields[1].startswith("ABSTRACT TABLE")
            ):
                m = re.search(r"\((.*)\)", fields[1])
                if m:
                    superclass = m.group(1)
                else:
                    superclass = None

                if len(fields) > 3 and fields[3].strip():
                    table_row = fields[3].strip()
                else:
                    table_row = fields[2]

                table = Table(
                    name=fields[2],
                    row=table_row,
                    superClass=superclass,
                    show=all_tables == [] or fields[2] in all_tables,
                    is_abstract=fields[1].startswith("ABSTRACT"),
                )
                tables[table.name.lower()] = table

            elif table and len(fields) > 3:
                # Ignores VIEWs
                fields[3] = fields[3].strip()

                if fields[1] == "FK:":
                    pairs = []
                    to = fields[4].strip().split()

                    for index, f in enumerate(fields[3].split()):
                        pairs.append((f, to[index]))

                    table.fk.append(Foreign_Key(table.name, fields[2], pairs, ""))

                elif fields[1] in ["INDEX:", "UNIQUE:"]:
                    # Skip, no influence here
                    pass

                else:
                    if fields[2].startswith("FK "):
                        table.fk.append(
                            Foreign_Key.parse_fk_descr(table, fields[1], fields[2])
                        )

                    null = (
                        fields[3].find("NOT NULL") == -1 and fields[3].find("PK") == -1
                    )

                    field = Field(
                        table=table,
                        name=fields[1],
                        type=fields[2],
                        default=fields[4].strip(),
                        comment=fields[5].strip(),
                        null=null,
                        show="%s.%s" % (table.name, fields[1]) not in omit,
                    )
                    table.fields.append(field)
                    if fields[3] == "PK":
                        field.pk = True
                        table.pk.append(field)

    # Now resolve all foreign keys

    for t in list(tables.values()):
        t.resolve_fk(tables)

    return tables


##########
# Graphs #
##########


class Graph(object):
    default_color = "white"
    bg_color = "palegoldenrod"
    font = "tahoma"

    @staticmethod
    def create(setup, output_file, clusters=None):
        # CLUSTERS can be used to group tables. Grouping is done either
        # just be settings a specific color for their title bars, but you can
        # also force graphviz to group them visually by calling the groups with
        # names starting with "cluster_".
        # The keys in this dict are the names of the group. Each value is a
        # tuple, whose first element is the color to use for that group, and
        # the remaining elements are the names of the database tables belonging
        # to that group. For instance:
        #    clusters ["group1"] = ("color", "table1", "table2")

        if clusters is None:
            clusters = {}

        tables = get_db_schema(setup)

        abs_output_file = os.path.abspath(output_file)
        output = open(abs_output_file, "w")
        output.write(
            """
digraph g {
  graph [
    rankdir = "TD",
    //rankdir = "LR",
    concentrate = true,     // Allow edge concentration
    //page="11.69291,16.53",  // Output on multiple pages (A3), inches
    size="15.5,20.5",       // Force output on single page (inches)
    rotate=90,              // Landscape mode
    nodesep=0,
    ranksep=0,
    margin=0
  ];

  node [
    fontsize = 8,
    fontname = "%(font)s",
    shape = plaintext
  ];

  edge [ ];

  "legend" [label=<<TABLE CELLBORDER="0" CELLSPACING="0">
<TR><TD>Legend</TD></TR>
"""
            % {"font": Graph.font}
        )

        for c in sorted(clusters.keys()):
            output.write('<TR><TD BGCOLOR="%s">%s</TD></TR>\n' % (clusters[c][0], c))

        output.write("""</TABLE>>, layer=0]""")

        colors = {}

        for c in list(clusters.keys()):
            t = clusters[c][1:]
            notfound = [ta for ta in t if ta not in tables]
            if notfound:
                print(
                    "Tables referenced in clusters, but no longer exists: %s"
                    % " ".join(notfound)
                )
            output.write(""" subgraph "%s" { %s }""" % (c, " ".join(t)))
            for col in t:
                colors[col.lower()] = clusters[c][0]

        for table in list(tables.values()):
            attrs = ""
            for field in table.fields:
                trans = {
                    "bg": Graph.bg_color,
                    "name": field.name,
                }  # + " : " + field.type.sql_type}

                if field.is_fk():
                    if field.is_pk():
                        html = '<FONT FACE="bold" COLOR="blue">%(name)s</FONT>'
                    else:
                        html = '<FONT COLOR="blue">%(name)s</FONT>'
                else:
                    if field.is_pk():
                        html = '<FONT FACE="bold">%(name)s</FONT>'
                    else:
                        html = "%(name)s"

                attrs += ('<TR><TD BGCOLOR="%(bg)s">' + html + "</TD></TR>") % trans

            output.write(
                """
 "%s" [label=<<TABLE BORDER="1" CELLBORDER="0" CELLSPACING="0"><TR>
                     <TD BGCOLOR="%s">%s</TD></TR>%s</TABLE>>]\n"""
                % (
                    table.name.lower(),
                    colors.get(table.name.lower(), Graph.default_color),
                    table.name,
                    attrs,
                )
            )

        for table in list(tables.values()):
            for f in table.fk:
                output.write(
                    '"%s" -> "%s";\n' % (table.name.lower(), f.foreign.name.lower())
                )

        output.write("}")
        output.close()

        abs_ps = abs_output_file.replace(".dot", ".ps")
        ps = output_file.replace(".dot", ".ps")
        try:
            sub = subprocess.Popen(["dot", "-Tps", "-o", abs_ps, abs_output_file])
            status = sub.wait()
        except OSError:
            status = 1

        if status != 0:
            print("Created '%s'" % output_file)
            print("Use 'dot -Tps -o %s %s' to convert to PS" % (ps, output_file))
        else:
            print("Created '%s'" % abs_ps)
        print("Use 'ps2pdf -sPAGESIZE=a3' to convert to PDF")


########
# Main #
########

if __name__ == "__main__":
    if len(sys.argv) < 3:
        print(
            "Usage: dborm -ada  dbschema.txt [pkg_name] [db_pkg]"
            + " [output_dir] [tables]"
        )
        print("   or: dborm -graph dbschema.txt [clusters]")
        print("")
        print("Where dbschema.txt contains the description of the database")
        print("as described in the Gnat Components Collection documentation.")
        print("")
        print("Clusters describes the grouping to use for tables in the")
        print("graph output. These are any number of arguments of the form:")
        print("    name:bg:table1,table2,table3,...")
        sys.exit(0)

    db = DBSetup.from_file(filename=sys.argv[2])

    if sys.argv[1] == "-ada":
        pkg = "%s.%s" % (pkg_name, "Queries")
        output_dir = "."

        if len(sys.argv) >= 4:
            pkg = sys.argv[3]
        if len(sys.argv) >= 5:
            database_pkg = sys.argv[4]
        if len(sys.argv) >= 6:
            output_dir = sys.argv[5]
        if len(sys.argv) >= 7 and sys.argv[6] != "":
            tables = sys.argv[6].split(",")
        else:
            tables = []

        sys.exit(create_orm(db, indir=output_dir, omit=[], pkg_name=pkg, tables=tables))

    elif sys.argv[1] == "-graph":
        clusters = {}
        for s in sys.argv[3:]:
            name, bg, tables = s.split(":")
            clusters[name] = [bg] + tables.split(",")
        Graph.create(db, output_file="schema.dot", clusters=clusters)
