#!/usr/bin/env qore
# -*- mode: qore; indent-tabs-mode: nil -*-

/** schema

    This example file shows how to use the Schema module to implement automatic
    schema management
*/

%new-style
%enable-all-warnings
%require-types
%strict-args

%requires DatasourceProvider

%requires Schema

# run our script logic in the main() function
main();

# GetOpt options
const Opts = (
    "drop": "D,drop",

    "data_ts": "d,data-ts=s",
    "index_ts": "i,index-ts=s",
    "verbose": "v,verbose:i+",

    "force": "f,force",
    "help": "h,help",
    );

sub usage() {
    printf("usage: %s [options] <datasource-spec>
    example datasource-specs:
       oracle:user/pass@db%example.com:1521
       pgsql:user/pass@db%localhost
 -D,--drop              drop the schema
 -d,--data-ts=<arg>     set the data tablespace name
 -f,--force             force the alignment
 -i,--index-ts=<arg>    set the index tablespace name
 -v,--verbose           show more output
 -h,--help              this help text
  ", get_script_name());
    exit(1);
}

class ExampleSchema inherits AbstractVersionedSchema {
    public {
        # the schema version
        const Version = "0.1";

        # index options
        const IndexOptions = (
            "driver": (
                "oracle": (
                    "compute_statistics": True,
                ),
            ),
            );

        # column options
        const ColumnOptions = (
            "driver": (
                "oracle": (
                    "character_semantics": True,
                ),
            ),
            );

        # table containing schema info including the schema version
        const T_SchemaProperties = (
            "columns": (
                "keyname": c_varchar(100, True, "the schema property key name; PK"),
                "value": c_varchar(200, "the schema property value"),
            ),
            "primary_key": ("name": "pk_schema_properties", "columns": "keyname"),
            "indexes": (
                # Oracle also needs an index to match the PK constraint
                "driver": (
                    "oracle": (
                        "pk_schema_properties": ("columns": ("keyname"), "unique": True),
                    ),
                ),
            ),
            );

        # customer type reference table
        const T_CustomerType = (
            "columns": (
                "id": c_number(True, "customer type value; 1 = wholesale, 2 = retail; PK"),
                "description": c_varchar(200, "customer type description"),
            ),
            "primary_key": ("name": "pk_customer_type", "columns": "id"),
            "indexes": (
                # Oracle also needs an index to match the PK constraint
                "driver": (
                    "oracle": (
                        "pk_customer_type": ("columns": ("id"), "unique": True),
                    ),
                ),
            ),
            );

        # customers table
        const T_Customers = (
            "columns": (
                "id": c_number(True, "customer ID number; PK") + (
                    # this column is normally populated from a sequence by a trigger, but mysql
                    # enforces not null constraints before "before insert" triggers are fired, so
                    # we can't use our emulated sequences on mysql with a not null constraint on this
                    # column, also since this column is a part of the primary key for this table,
                    # we can't leave it nullable, so we use auto_increment
                    "driver": ("mysql": ("native_type": "bigint", "unsigned": True, "auto_increment": True, /*"size": NOTHING*/)),
                ),
                "customer_type": c_number(True, "customer type ID, FK to customer_type"),
                "name": c_varchar(120, True, "customer name"),
                "created": c_timestamp(True, "creation date/time") + (
                    # this column is populated by a trigger, but mysql enforces not null
                    # constraints before "before insert" triggers are fired, so for mysql only
                    # this column must be nullable
                    "driver": ("mysql": ("notnull": False)),
                ),
                "modified": c_timestamp("modified date/time"),
            ),
            "primary_key": ("name": "pk_customers", "columns": "id"),
            "indexes": (
                "sk_customers_name": ("columns": "name"),
                # Oracle also needs an index to match the PK constraint
                "driver": (
                    "oracle": (
                        "pk_customers": ("columns": ("id"), "unique": True),
                    ),
                ),
            ),
            # define our constraint on the customer_type table
            "foreign_constraints": (
                "fk_cust_cust_type": ("columns": "customer_type", "table": "customer_type", "target_columns": "id"),
            ),
            # DB-specific trigger code
            "driver": (
                "pgsql": (
                    "functions": (
                        "trig_customers()": "returns trigger language plpgsql as $function$
begin
  if (tg_op = 'INSERT') then
    if new.created is null then
      select current_timestamp into new.created;
    end if;
  end if;
  if new.modified is null  then
    select current_timestamp into new.modified;
  end if;
  return new;
end;
$function$", #",
                    ),
                ),
            ),
            "triggers": (
                "driver": (
                    "oracle": (
                        "trig_customers": "BEFORE INSERT OR UPDATE ON customers
REFERENCING NEW AS NEW OLD AS OLD
FOR EACH ROW
begin
  if inserting then
    if :new.id is null then
      select seq_customers.nextval into :new.id from dual;
    end if;
    if :new.created is null then
      :new.created := sysdate;
    end if;
  end if;
  --
  if :new.modified is null or :new.modified = :old.modified then
    :new.modified := sysdate;
  end if;
end;",
                    ),
                    "pgsql": (
                        "trig_customers": "before insert or update on customers for each row execute procedure trig_customers()",
                    ),
                    "mysql": (
                        "trig_customers_insert": "before insert on customers for each row
begin
  if new.created is null then
    set new.created = now();
  end if;
  if new.modified is null then
    set new.modified = now();
  end if;
end",
                        "trig_customers_update": "before update on customers for each row
begin
  if new.modified is null or new.modified = old.modified then
    set new.modified = now();
  end if;
end",
                    ),
                ),
            ),
            );

        # list of all sequences
        const SequenceList = (
            "seq_customers": {},
            );

        # sequences only used for oracle & pgsql
        const Sequences = (
            "driver": (
                "oracle": SequenceList,
                "pgsql": SequenceList,
            ),
            );

        # hash of all tables in the schema
        const Tables = (
            "schema_properties": T_SchemaProperties,
            "customer_type": T_CustomerType,
            "customers": T_Customers,
            );

        # reference data for the schema_properties table
        const SRD_SchemaProperties = (
            ("keyname", "value"),
            ("version", Version),
            );

        # reference data for the customer_type table
        const SRD_CustomerType = (
            ("id", "description"),
            (1, "wholesale customer"),
            (2, "retail customer"),
            );

        # hash of strict reference data, describing the only data that can appear in the given tables
        const StrictReferenceData = (
            "schema_properties": SRD_SchemaProperties,
            "customer_type": SRD_CustomerType,
            );
    }

    # creates the object
    constructor(AbstractDatasource ds, *string dts, *string its, *hash opts) : AbstractVersionedSchema(ds, dts, its, opts) {
    }

    # returns the name of the schema
    string getNameImpl() {
        return "ExampleSchema";
    }

    # returns the version of the schema
    string getVersionImpl() {
        return Version;
    }

    # returns the name of the table holding the schema version string
    string getVersionTableImpl() {
        return "schema_properties";
    }

    # returns the name of the column holding the schema version string
    string getVersionColumnImpl() {
        return "value";
    }

    # returns the where clause hash defining the row where the schema version string is located
    hash getVersionWhereImpl() {
        return ("keyname": "version");
    }

    # returns the table descriptions for our schema
    private *hash getTablesImpl() {
        return Tables;
    }

    # returns the sequence descriptions for our schema
    private *hash getSequencesImpl() {
        return Sequences;
    }

    # returns the "strict reference data" for our schema, describing the only data that can be in the tables
    private *hash getStrictReferenceDataHashImpl() {
        return StrictReferenceData;
    }

    # returns a hash of column options
    private *hash getColumnOptionsImpl() {
        return ColumnOptions;
    }

    # returns a hash of index options
    private *hash getIndexOptionsImpl() {
        return IndexOptions;
    }
}

sub main() {
    GetOpt g(Opts);
    *hash o = g.parse3(\ARGV);
    if (o.help)
        usage();

    *string dsstr = shift ARGV;
    if (!dsstr)
        usage();

    Datasource ds(get_ds_string(dsstr));

    ExampleSchema schema(ds, o.data_ts, o.index_ts);

    on_success ds.commit();
    on_error ds.rollback();

    if (o.drop) {
        schema.drop(o.force, o.verbose);
        return;
    }

    schema.align(o.force, o.verbose);
}
