tgqe 0.0.3

The Great Qin Empire —— A centralized system for integrating and summarizing common compiler front-end library errors and span error output libraries, with optional Ariadne integration.
/**

 *  - Copyright (c) 星灿长风v(Starwindv) 2026/08/03
 *  - License: BSD-3
 *  - Author: 星灿长风v(StarWindv)
 *  - Location: tgqe/src/modules/channel/sqlite.rs
 */

use std::env;
use std::path::PathBuf;

use sqlx::SqlitePool;
use sqlx_sqlite::{
    SqliteConnectOptions, SqlitePoolOptions,
};

use crate::base_types::TgqeCtx;
use crate::enums::TgqeLevelFilter;
use crate::singletons::Configure;

/**

 * ## I. What
 *
 * ### 1.1 Description
 *
 * This structure is the database channel of this library.
 *
 * It lazily creates a sqlite connection pool and stores
 * error records into the `tgqe_errors` table.
 *
 * ---
 *
 * ## II. Memory Layout
 *
 * ### 2.1 Analyze
 *
 * ```text
 * SQLite <
 *     pool   : Option<SqlitePool>, // 16
 *     db_path: PathBuf,            // 24
 * > // 40
 * ```
 *
 * ---
 *
 * ## III. Default Value:
 * - pool   : None
 * - db_path: `{cwd}/{TGQE_DB_FILE_FORMAT}.sqlite`
 */
pub struct SQLite {
    pub pool: Option<SqlitePool>,
    pub db_path: PathBuf,
}

const CREATE_TABLE_SQL: &str = "CREATE TABLE IF NOT EXISTS tgqe_errors (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    filepath TEXT NOT NULL,
    publisher TEXT NOT NULL,
    level TEXT NOT NULL,
    start_offset INTEGER NOT NULL,
    end_offset INTEGER NOT NULL,
    expect TEXT NOT NULL,
    got TEXT NOT NULL,
    err_type TEXT NOT NULL,
    recoverable INTEGER NOT NULL,
    hints TEXT NOT NULL,
    labels TEXT NOT NULL,
    timestamp INTEGER NOT NULL
)";

const CREATE_INDEX_SQL: [&str; 6] = [
    "CREATE INDEX IF NOT EXISTS idx_tgqe_errors_filepath ON tgqe_errors (filepath)",
    "CREATE INDEX IF NOT EXISTS idx_tgqe_errors_publisher ON tgqe_errors (publisher)",
    "CREATE INDEX IF NOT EXISTS idx_tgqe_errors_level ON tgqe_errors (level)",
    "CREATE INDEX IF NOT EXISTS idx_tgqe_errors_err_type ON tgqe_errors (err_type)",
    "CREATE INDEX IF NOT EXISTS idx_tgqe_errors_recoverable ON tgqe_errors (recoverable)",
    "CREATE INDEX IF NOT EXISTS idx_tgqe_errors_timestamp ON tgqe_errors (timestamp)",
];

const INSERT_SQL: &str = "
INSERT INTO tgqe_errors (
            filepath, publisher, level,
            start_offset, end_offset,
            expect, got, err_type,
            recoverable, hints, labels, timestamp
        ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
";

impl SQLite {
    pub fn new() -> Self {
        let cwd = env::current_dir()
            .unwrap_or_else(|_| {
                PathBuf::from("../../../..")
            });
        let db_path = cwd.join(format!(
            "{}.sqlite",
            chrono::Local::now().format(
                &Configure.db_file_format
            )
        ));
        Self {
            pool: None,
            db_path,
        }
    }

    pub(crate) async fn lazy_create(
        &mut self,
    ) -> Result<(), sqlx::Error> {
        if self.pool.is_some() {
            return Ok(());
        }

        let options =
            SqliteConnectOptions::new()
                .filename(&self.db_path)
                .create_if_missing(true);

        let pool = SqlitePoolOptions::new()
            .max_connections(
                Configure.db_max_connections,
            )
            .connect_with(options)
            .await?;

        self.pool = Some(pool);
        Ok(())
    }

    pub(crate) async fn init(
        &mut self,
    ) -> Result<(), sqlx::Error> {
        self.lazy_create().await?;
        let pool = self
            .pool
            .as_ref()
            .expect("This error should not have occurred.");

        sqlx::query(CREATE_TABLE_SQL)
            .execute(pool)
            .await?;
        for index_sql in CREATE_INDEX_SQL {
            sqlx::query(index_sql)
                .execute(pool)
                .await?;
        }
        Ok(())
    }

    pub async fn save(
        &mut self,
        error: &TgqeCtx,
    ) -> Result<(), sqlx::Error> {
        self.init().await?;
        let pool =
            self.pool.as_ref().expect(
                "This error should not have occurred.",
            );
        Self::insert_one(error, pool).await
    }

    pub async fn batch_save(
        &mut self,
        errors: &[TgqeCtx],
    ) -> Result<(), sqlx::Error> {
        self.init().await?;
        let pool =
            self.pool.as_ref().expect(
                "This error should not have occurred.",
            );

        let mut tx = pool.begin().await?;
        for error in errors {
            Self::insert_one(
                error, &mut *tx,
            )
            .await?;
        }
        tx.commit().await?;
        Ok(())
    }

    fn level_str(
        level: TgqeLevelFilter,
    ) -> &'static str {
        match level {
            TgqeLevelFilter::Error => {
                "Error"
            }
            TgqeLevelFilter::Warn => "Warn",
            TgqeLevelFilter::Info => "Info",
            TgqeLevelFilter::Undefined => {
                "Undefined"
            }
        }
    }

    async fn insert_one<'e, E>(
        error: &TgqeCtx,
        executor: E,
    ) -> Result<(), sqlx::Error>
    where
        E: sqlx::Executor<
                'e,
                Database = sqlx::Sqlite,
            >,
    {
        let timestamp =
            if error.ns_timestamp != 0 {
                error.ns_timestamp
            } else {
                chrono::Local::now()
                    .timestamp_nanos_opt()
                    .unwrap_or(0)
            };

        sqlx::query(INSERT_SQL)
            .bind(
                error
                    .position
                    .coord
                    .filepath
                    .as_str(),
            )
            .bind(error.publisher.as_str())
            .bind(Self::level_str(
                error.err_info.level,
            ))
            .bind(
                error
                    .position
                    .span
                    .start
                    .offset
                    as i64,
            )
            .bind(
                error
                    .position
                    .span
                    .end
                    .offset
                    as i64,
            )
            .bind(
                error
                    .err_info
                    .expect
                    .as_str(),
            )
            .bind(
                error.err_info.got.as_str(),
            )
            .bind(
                error
                    .err_info
                    .err_type
                    .as_str(),
            )
            .bind(error.err_info.recoverable)
            .bind(error.hints.as_str())
            .bind(error.labels.as_str())
            .bind(timestamp)
            .execute(executor)
            .await?;

        Ok(())
    }
}