'Like `.insert_or_update()`, but `specs` parameter has to be an
iterable of `(timestamp, spec)` (if `separated_timestamps` is
True) or `spec` (if it is False) values. This will perform
PostgreSQL COPY FROM inserts with the major drawback that the
`getinfos` parameter will be called (if it is not `None`) for
each spec, even when the spec already exists in the database
and the call was hence unnecessary.
It\'s up to you to decide whether having bulk insert is worth
it or if you want to go with the regular `.insert_or_update()`
method.'

def insert_or_update_bulk(self, specs, getinfos=None, separated_timestamps=True):

    more_to_read = True
    tmp = self.create_tmp_table(Passive, extracols=[Column('addr', postgresql.INET), Column('context', String(32))])
    if config.DEBUG_DB:
        total_upserted = 0
        total_start_time = time.time()
    while more_to_read:
        if config.DEBUG_DB:
            start_time = time.time()
        with PassiveCSVFile(specs, self.get_context, tmp, getinfos=getinfos, separated_timestamps=separated_timestamps, limit=config.POSTGRES_BATCH_SIZE) as fdesc:
            self.copy_from(fdesc, tmp.name)
            more_to_read = fdesc.more_to_read
            if config.DEBUG_DB:
                count_upserted = fdesc.count
        self.db.execute(postgresql.insert(Context).from_select(['name'], select([column('context')]).select_from(tmp).where(tmp.columns['context'].isnot(null())).distinct(column('context'))).on_conflict_do_nothing())
        insrt = postgresql.insert(Host)
        self.db.execute(insrt.from_select([column(col) for col in ['context', 'addr', 'firstseen', 'lastseen']], select([Context.id, column('addr'), func.min_(column('firstseen')), func.max_(column('lastseen'))]).select_from(join(Context, tmp, (Context.name == column('context')))).where(tmp.columns['addr'].isnot(null())).group_by(Context.id, column('addr'))).on_conflict_do_update(index_elements=['addr', 'context'], set_={'firstseen': func.least(Host.firstseen, insrt.excluded.firstseen), 'lastseen': func.greatest(Host.lastseen, insrt.excluded.lastseen)}))
        insrt = postgresql.insert(Passive)
        self.db.execute(insrt.from_select([column(col) for col in ['host', 'count', 'firstseen', 'lastseen', 'sensor', 'port', 'recontype', 'source', 'targetval', 'value', 'fullvalue', 'info', 'moreinfo']], select(([Host.id, func.sum_(tmp.columns['count']), func.min_(tmp.columns['firstseen']), func.max_(tmp.columns['lastseen'])] + [tmp.columns[col] for col in ['sensor', 'port', 'recontype', 'source', 'targetval', 'value', 'fullvalue', 'info', 'moreinfo']])).select_from(join(tmp, join(Context, Host), (((Context.name == tmp.columns['context']) | (Context.name.is_(null()) & tmp.columns['context'].is_(null()))) & ((Host.addr == tmp.columns['addr']) | (Host.addr.is_(null()) & tmp.columns['addr'].is_(null())))))).group_by(Host.id, *(tmp.columns[col] for col in ['sensor', 'port', 'recontype', 'source', 'targetval', 'value', 'fullvalue', 'info', 'moreinfo']))).on_conflict_do_update(index_elements=['host', 'sensor', 'recontype', 'port', 'source', 'value', 'targetval', 'info'], set_={'firstseen': func.least(Passive.firstseen, insrt.excluded.firstseen), 'lastseen': func.greatest(Passive.lastseen, insrt.excluded.lastseen), 'count': (Passive.count + insrt.excluded.count)}))
        self.db.execute(delete(tmp))
        if config.DEBUG_DB:
            stop_time = time.time()
            time_spent = (stop_time - start_time)
            total_upserted += count_upserted
            total_time_spent = (stop_time - total_start_time)
            utils.LOGGER.debug('DB:PERFORMANCE    STATS    %s    upserts,    %f    s,    %s/s\n DCTB total:    %s    upserts,    %f    s,    %s/s', utils.num2readable(count_upserted), time_spent, utils.num2readable((count_upserted / time_spent)), utils.num2readable(total_upserted), total_time_spent, utils.num2readable((total_upserted / total_time_spent)))
