geolite 0.0.10

open source geocode solution
use rusqlite::Connection;
use std::collections::HashMap;

use super::entity::{decode_chain, hierarchy_lookup_row, hierarchy_row};
use crate::domain::admin_level::level;
use crate::domain::table;

const SQL_CREATE: &str = "
  CREATE TABLE IF NOT EXISTS admin_levels_hierarchy (
    admin_level_id INTEGER PRIMARY KEY REFERENCES admin_levels(id) ON DELETE CASCADE,
    ancestor_ids BLOB NOT NULL,
    user_friendly_name TEXT NOT NULL
  );
";

const SQL_DROP: &str = "DROP TABLE IF EXISTS admin_levels_hierarchy;";

pub struct admin_levels_hierarchy;

impl table for admin_levels_hierarchy {
  const CREATE: &str = SQL_CREATE;
  const INDEXES: &str = "";
}

pub(crate) fn drop_table(conn: &Connection) {
  conn
    .execute_batch(SQL_DROP)
    .expect("failed to drop admin_levels_hierarchy");
}

pub fn destroy(conn: &Connection) {
  drop_table(conn);
  admin_levels_hierarchy::create_table(conn);
}

pub fn count(conn: &Connection) -> i64 {
  const SQL_COUNT: &str = "
    SELECT COUNT(*) FROM admin_levels_hierarchy
  ";

  conn
    .query_row(SQL_COUNT, [], |row| row.get(0))
    .expect("failed to count admin_levels_hierarchy")
}

pub fn pending_total(conn: &Connection) -> i64 {
  const SQL_PENDING_TOTAL: &str = "
    WITH pending AS (
      SELECT al.id
      FROM admin_levels al
      LEFT JOIN admin_levels_hierarchy h ON al.id = h.admin_level_id
      WHERE h.admin_level_id IS NULL
    )
    SELECT COUNT(*) FROM pending
  ";

  conn
    .query_row(SQL_PENDING_TOTAL, [], |row| row.get::<_, i64>(0))
    .expect("failed to query pending total")
}

pub fn pending_street_ids(conn: &Connection) -> Vec<i64> {
  const SQL_PENDING_STREET_IDS: &str = "
    WITH already_indexed AS (
      SELECT admin_level_id FROM admin_levels_hierarchy
    )
    SELECT al.id
    FROM admin_levels al
    LEFT JOIN already_indexed ai ON al.id = ai.admin_level_id
    WHERE al.admin_level = ?1
      AND ai.admin_level_id IS NULL
    ORDER BY al.id ASC
  ";

  let mut stmt = conn
    .prepare(SQL_PENDING_STREET_IDS)
    .expect("failed to prepare pending streets");
  stmt
    .query_map([level::street.value()], |row| row.get::<_, i64>(0))
    .expect("failed to query pending streets")
    .map(|r| r.expect("failed to read street id"))
    .collect()
}

pub fn load_by_ids(conn: &Connection, ids: &[i64]) -> HashMap<i64, hierarchy_lookup_row> {
  const SQL_LOAD_BY_IDS: &str = "
    SELECT admin_level_id, user_friendly_name, json(ancestor_ids)
    FROM admin_levels_hierarchy
    WHERE admin_level_id IN
  ";

  if ids.is_empty() {
    return HashMap::new();
  }
  let sql = format!(
    "{} ({})",
    SQL_LOAD_BY_IDS.trim(),
    crate::database::placeholders_for(ids.len())
  );
  crate::database::query_by_ids(conn, &sql, ids.iter().copied(), |row| {
    let chain: String = row.get(2)?;
    Ok((
      row.get::<_, i64>(0)?,
      hierarchy_lookup_row {
        user_friendly_name: row.get(1)?,
        ancestor_ids: decode_chain(&chain),
      },
    ))
  })
  .into_iter()
  .collect()
}

pub fn batch_insert(conn: &Connection, rows: &[hierarchy_row]) {
  const SQL_INSERT: &str = "
    INSERT OR IGNORE INTO admin_levels_hierarchy (
      admin_level_id,
      ancestor_ids,
      user_friendly_name
    ) VALUES (
      ?1,
      jsonb(?2),
      ?3
    );
  ";

  if rows.is_empty() {
    return;
  }
  let tx = conn
    .unchecked_transaction()
    .expect("failed to begin transaction");
  {
    let mut stmt = tx
      .prepare(SQL_INSERT)
      .expect("failed to prepare hierarchy insert");
    rows
      .iter()
      .try_for_each(|row| {
        stmt
          .execute(rusqlite::params![
            row.admin_level_id,
            row.ancestor_ids,
            row.user_friendly_name
          ])
          .map(|_| ())
      })
      .expect("failed to insert hierarchy row");
  }
  tx.commit().expect("failed to commit");
}

#[cfg(test)]
#[path = "repository.test.rs"]
mod tests;