-- This file has functions that implement changes to hypertable columns as
-- change to the underlying chunk tables.

-- TODO(mat) - Doc this? Not sure I can do it justice.
CREATE OR REPLACE FUNCTION _sysinternal.create_partition_constraint_for_field(
    hypertable_name NAME,
    field_name      NAME
)
    RETURNS VOID LANGUAGE PLPGSQL VOLATILE AS
$BODY$
BEGIN
    PERFORM _sysinternal.add_partition_constraint(pr.schema_name, pr.table_name, p.keyspace_start, p.keyspace_end,
                                                  p.epoch_id)
    FROM partition_epoch pe
    INNER JOIN partition p ON (p.epoch_id = pe.id)
    INNER JOIN partition_replica pr ON (pr.partition_id = p.id)
    WHERE pe.hypertable_name = create_partition_constraint_for_field.hypertable_name
          AND pe.partitioning_field = create_partition_constraint_for_field.field_name;
END
$BODY$;

-- Adds a field to a table (e.g. main table or root table)
CREATE OR REPLACE FUNCTION _sysinternal.create_field_on_table(
    schema_name   NAME,
    table_name    NAME,
    field         NAME,
    attnum        int2,
    data_type_oid REGTYPE,
    default_value TEXT,
    not_null      BOOLEAN
)
    RETURNS VOID LANGUAGE PLPGSQL VOLATILE AS
$BODY$
DECLARE
  null_constraint         TEXT := 'NOT NULL';
  default_constraint      TEXT := '';
  created_columns_att_num INT2;
BEGIN
    IF NOT not_null THEN
      null_constraint = 'NULL';
    END IF;

    default_constraint = 'DEFAULT '|| default_value;

    EXECUTE format(
        $$
            ALTER TABLE %1$I.%2$I ADD COLUMN %3$I %4$s %5$s %6$s
        $$,
        schema_name, table_name, field, data_type_oid, default_constraint, null_constraint);

    SELECT att.attnum INTO STRICT created_columns_att_num
    FROM pg_attribute att
    WHERE att.attrelid = format('%I.%I', schema_name, table_name)::regclass AND att.attname = field
    AND NOT attisdropped;

    IF created_columns_att_num IS DISTINCT FROM attnum THEN
      RAISE EXCEPTION 'Inconsistent state: the attnum of newly created colum does not match (% vs %)', attnum, created_columns_att_num
      USING ERRCODE = 'IO501';
    END IF;
END
$BODY$;


-- Removes a field from a table (e.g. main table or root table)
CREATE OR REPLACE FUNCTION _sysinternal.drop_field_on_table(
    schema_name   NAME,
    table_name    NAME,
    field         NAME
) RETURNS VOID LANGUAGE PLPGSQL VOLATILE AS
$BODY$
BEGIN
    EXECUTE format(
        $$
            ALTER TABLE %1$I.%2$I DROP COLUMN %3$I
        $$,
        schema_name, table_name, field);
END
$BODY$;

-- Changes the default of a field in a table (e.g. main table or root table)
CREATE OR REPLACE FUNCTION _sysinternal.exec_alter_column_set_default(
    schema_name   NAME,
    table_name    NAME,
    field         NAME,
    new_default_value TEXT
) RETURNS VOID LANGUAGE PLPGSQL VOLATILE AS
$BODY$
BEGIN
    EXECUTE format(
        $$
            ALTER TABLE %1$I.%2$I ALTER COLUMN %3$I SET DEFAULT %4$L
        $$,
        schema_name, table_name, field, new_default_value);
END
$BODY$;

-- Renames a field of a table (e.g. main table or root table)
CREATE OR REPLACE FUNCTION _sysinternal.exec_alter_table_rename_column(
    schema_name   NAME,
    table_name    NAME,
    old_field     NAME,
    new_field     NAME
) RETURNS VOID LANGUAGE PLPGSQL VOLATILE AS
$BODY$
BEGIN
    EXECUTE format(
        $$
            ALTER TABLE %1$I.%2$I RENAME COLUMN %3$I TO %4$I
        $$,
        schema_name, table_name, old_field, new_field);
END
$BODY$;

-- Sets a field of a table (e.g. main table or root table) to NOT NULL
CREATE OR REPLACE FUNCTION _sysinternal.exec_alter_column_set_not_null(
    schema_name   NAME,
    table_name    NAME,
    field         NAME,
    new_not_null  BOOLEAN
) RETURNS VOID LANGUAGE PLPGSQL VOLATILE AS
$BODY$
BEGIN
IF new_not_null THEN
  EXECUTE format(
    $$
        ALTER TABLE %1$I.%2$I ALTER COLUMN %3$I SET NOT NULL
    $$,
    schema_name, table_name, field);
ELSE
  EXECUTE format(
    $$
        ALTER TABLE %1$I.%2$I ALTER COLUMN %3$I DROP NOT NULL
    $$,
    schema_name, table_name, field);
END IF;
END
$BODY$;

-- Adds distinct values for a field to a hypertable's distinct table.
CREATE OR REPLACE FUNCTION _sysinternal.populate_distinct_table(
    hypertable_name  NAME,
    field            NAME
) RETURNS VOID LANGUAGE PLPGSQL VOLATILE AS
$BODY$
DECLARE
   distinct_replica_node_row  distinct_replica_node;
   chunk_replica_node_row     chunk_replica_node;
BEGIN
  FOR distinct_replica_node_row IN
    SELECT *
    FROM distinct_replica_node drn
    WHERE drn.hypertable_name = populate_distinct_table.hypertable_name AND
          drn.database_name = current_database()
    LOOP
      FOR chunk_replica_node_row IN
        SELECT crn.*
        FROM chunk_replica_node crn
        INNER JOIN partition_replica pr ON (pr.id = crn.partition_replica_id)
        WHERE pr.hypertable_name = distinct_replica_node_row.hypertable_name AND
              pr.replica_id = distinct_replica_node_row.replica_id AND
              crn.database_name = current_database()
        LOOP
            EXECUTE format($$
                INSERT INTO %I.%I(field, value)
                   SELECT DISTINCT %L, %I
                   FROM %I.%I
                ON CONFLICT DO NOTHING
              $$,
              distinct_replica_node_row.schema_name, distinct_replica_node_row.table_name,
              field, field,
              chunk_replica_node_row.schema_name, chunk_replica_node_row.table_name
            );
        END LOOP;
    END LOOP;
END
$BODY$;

-- Removes distinct values for a field from a hypertable's distinct table.
CREATE OR REPLACE FUNCTION _sysinternal.unpopulate_distinct_table(
    hypertable_name  NAME,
    field            NAME
) RETURNS VOID LANGUAGE PLPGSQL VOLATILE AS
$BODY$
DECLARE
   distinct_replica_node_row  distinct_replica_node;
BEGIN
  FOR distinct_replica_node_row IN
    SELECT *
    FROM distinct_replica_node drn
    WHERE drn.hypertable_name = unpopulate_distinct_table.hypertable_name AND
          drn.database_name = current_database()
    LOOP
        EXECUTE format($$
                DELETE FROM %I.%I WHERE field = %L
          $$,
          distinct_replica_node_row.schema_name, distinct_replica_node_row.table_name, field
        );
    END LOOP;
END
$BODY$;

-- Trigger to modify a field from a hypertable.
-- Called when the user alters the main table by adding a field or changing
-- the properties of a field.
CREATE OR REPLACE FUNCTION _sysinternal.on_modify_field()
    RETURNS TRIGGER LANGUAGE PLPGSQL AS
$BODY$
DECLARE
    hypertable_row hypertable;
    update_found   BOOLEAN = false;
BEGIN

    IF TG_OP = 'INSERT' THEN
      SELECT *
      INTO STRICT hypertable_row
      FROM hypertable AS h
      WHERE h.name = NEW.hypertable_name;

      -- update root table
      PERFORM _sysinternal.create_field_on_table(hypertable_row.root_schema_name, hypertable_row.root_table_name,
                                                NEW.name, NEW.attnum, NEW.data_type, NEW.default_value, NEW.not_null);
      IF new.created_on <> current_database() THEN
        PERFORM set_config('io.ignore_ddl_in_trigger', 'true', true);
        -- update main table on others
        PERFORM _sysinternal.create_field_on_table(hypertable_row.main_schema_name, hypertable_row.main_table_name,
                                                  NEW.name, NEW.attnum, NEW.data_type, NEW.default_value, NEW.not_null);
     END IF;

      PERFORM _sysinternal.create_partition_constraint_for_field(NEW.hypertable_name, NEW.name);
      RETURN NEW;
    ELSIF TG_OP = 'UPDATE' THEN
      SELECT *
      INTO STRICT hypertable_row
      FROM hypertable AS h
      WHERE h.name = NEW.hypertable_name;

      IF NEW.default_value IS DISTINCT FROM OLD.default_value THEN
        update_found = TRUE;
        -- update root table
        PERFORM _sysinternal.exec_alter_column_set_default(hypertable_row.root_schema_name, hypertable_row.root_table_name,
                                              NEW.name, NEW.default_value);
        IF NEW.modified_on <> current_database() THEN
          PERFORM set_config('io.ignore_ddl_in_trigger', 'true', true);
          -- update main table on others
          PERFORM _sysinternal.exec_alter_column_set_default(hypertable_row.main_schema_name, hypertable_row.main_table_name,
                                              NEW.name, NEW.default_value);
        END IF;
      END IF;
      IF NEW.not_null IS DISTINCT FROM OLD.not_null THEN
        update_found = TRUE;
        -- update root table
        PERFORM _sysinternal.exec_alter_column_set_not_null(hypertable_row.root_schema_name, hypertable_row.root_table_name,
                                              NEW.name, NEW.not_null);
        IF NEW.modified_on <> current_database() THEN
          PERFORM set_config('io.ignore_ddl_in_trigger', 'true', true);
          -- update main table on others
          PERFORM _sysinternal.exec_alter_column_set_not_null(hypertable_row.main_schema_name, hypertable_row.main_table_name,
                                              NEW.name, NEW.not_null);
        END IF;
      END IF;
      IF NEW.name IS DISTINCT FROM OLD.name THEN
        update_found = TRUE;
        -- update root table
        PERFORM _sysinternal.exec_alter_table_rename_column(hypertable_row.root_schema_name, hypertable_row.root_table_name,
                                              OLD.name, NEW.name);
        IF NEW.modified_on <> current_database() THEN
          PERFORM set_config('io.ignore_ddl_in_trigger', 'true', true);
          -- update main table on others
          PERFORM _sysinternal.exec_alter_table_rename_column(hypertable_row.main_schema_name, hypertable_row.main_table_name,
                                              OLD.name, NEW.name);
        END IF;
      END IF;

      IF NEW.is_distinct IS DISTINCT FROM OLD.is_distinct THEN
          update_found = TRUE;
          IF NEW.is_distinct THEN
            PERFORM  _sysinternal.populate_distinct_table(NEW.hypertable_name, NEW.name);
          ELSE
            PERFORM  _sysinternal.unpopulate_distinct_table(NEW.hypertable_name, NEW.name);
          END IF;
      END IF;

      IF NOT update_found THEN
        RAISE EXCEPTION 'Invalid update type on %', TG_TABLE_NAME
        USING ERRCODE = 'IO101';
      END IF;
      RETURN NEW;
    ELSIF TG_OP = 'DELETE' THEN
      --handled by deleted log
      RETURN OLD;
    END IF;
END
$BODY$
SET SEARCH_PATH = 'public';

-- Trigger to remove a field from a hypertable.
-- Called when the user alters the main table by deleting a field.
CREATE OR REPLACE FUNCTION _sysinternal.on_deleted_field()
    RETURNS TRIGGER LANGUAGE PLPGSQL AS
$BODY$
DECLARE
    hypertable_row hypertable;
BEGIN
    IF TG_OP <> 'INSERT' THEN
        RAISE EXCEPTION 'Only inserts supported on % table', TG_TABLE_NAME
        USING ERRCODE = 'IO101';
    END IF;

    SELECT *
    INTO hypertable_row
    FROM hypertable AS h
    WHERE h.name = NEW.hypertable_name;

    IF hypertable_row IS NULL THEN
        --presumably hypertable has been dropped and this is part of cascade
        RETURN NEW;
    END IF;

    -- update root table
    PERFORM _sysinternal.drop_field_on_table(hypertable_row.root_schema_name, hypertable_row.root_table_name,
                                              NEW.name);
    IF NEW.deleted_on <> current_database() THEN
      PERFORM set_config('io.ignore_ddl_in_trigger', 'true', true);
      -- update main table on others
      PERFORM _sysinternal.drop_field_on_table(hypertable_row.main_schema_name, hypertable_row.main_table_name,
                                              NEW.name);
    END IF;

    RETURN NEW;
END
$BODY$
SET SEARCH_PATH = 'public';
