<commit_before>extern crate mysqlclient_sys as ffi;

use std::ffi::CStr;
use std::os::raw as libc;
use std::ptr::{self, NonNull};
use std::sync::{Once, ONCE_INIT};

use super::stmt::Statement;
use super::url::ConnectionOptions;
use result::{ConnectionError, ConnectionResult, QueryResult};

pub struct RawConnection(NonNull<ffi::MYSQL>);

impl RawConnection {
    pub fn new() -> Self {
        perform_thread_unsafe_library_initialization();
        let raw_connection = unsafe { ffi::mysql_init(ptr::null_mut()) };
        // We're trusting https://dev.mysql.com/doc/refman/5.7/en/mysql-init.html
        // that null return always means OOM
        let raw_connection =
            NonNull::new(raw_connection).expect("Insufficient memory to allocate connection");
        let result = RawConnection(raw_connection);

        // This is only non-zero for unrecognized options, which should never happen.
        let charset_result = unsafe {
            ffi::mysql_options(
                result.0.as_ptr(),
                ffi::mysql_option::MYSQL_SET_CHARSET_NAME,
                b"utf8mb4\0".as_ptr() as *const libc::c_void,
            )
        };
        assert_eq!(
            0, charset_result,
            "MYSQL_SET_CHARSET_NAME was not \
             recognized as an option by MySQL. This should never \
             happen."
        );

        result
    }

    pub fn connect(&self, connection_options: &ConnectionOptions) -> ConnectionResult<()> {
        let host = connection_options.host();
        let user = connection_options.user();
        let password = connection_options.password();
        let database = connection_options.database();
        let port = connection_options.port();

        unsafe {
            // Make sure you don't use the fake one!
            ffi::mysql_real_connect(
                self.0.as_ptr(),
                host.map(CStr::as_ptr).unwrap_or_else(|| ptr::null_mut()),
                user.as_ptr(),
                password
                    .map(CStr::as_ptr)
                    .unwrap_or_else(|| ptr::null_mut()),
                database
                    .map(CStr::as_ptr)
                    .unwrap_or_else(|| ptr::null_mut()),
                u32::from(port.unwrap_or(0)),
                ptr::null_mut(),
                0,
            )
        };

        let last_error_message = self.last_error_message();
        if last_error_message.is_empty() {
            Ok(())
        } else {
            Err(ConnectionError::BadConnection(last_error_message))
        }
    }

    pub fn last_error_message(&self) -> String {
        unsafe { CStr::from_ptr(ffi::mysql_error(self.0.as_ptr())) }
            .to_string_lossy()
            .into_owned()
    }

    pub fn execute(&self, query: &str) -> QueryResult<()> {
        unsafe {
            // Make sure you don't use the fake one!
            ffi::mysql_real_query(
                self.0.as_ptr(),
                query.as_ptr() as *const libc::c_char,
                query.len() as libc::c_ulong,
            );
        }
        self.did_an_error_occur()?;
        self.flush_pending_results()?;
        Ok(())
    }

    pub fn enable_multi_statements<T, F>(&self, f: F) -> QueryResult<T>
    where
        F: FnOnce() -> QueryResult<T>,
    {
        unsafe {
            ffi::mysql_set_server_option(
                self.0.as_ptr(),
                ffi::enum_mysql_set_option::MYSQL_OPTION_MULTI_STATEMENTS_ON,
            );
        }
        self.did_an_error_occur()?;

        let result = f();

        unsafe {
            ffi::mysql_set_server_option(
                self.0.as_ptr(),
                ffi::enum_mysql_set_option::MYSQL_OPTION_MULTI_STATEMENTS_OFF,
            );
        }
        self.did_an_error_occur()?;

        result
    }

    pub fn affected_rows(&self) -> usize {
        let affected_rows = unsafe { ffi::mysql_affected_rows(self.0.as_ptr()) };
        affected_rows as usize
    }

    pub fn prepare(&self, query: &str) -> QueryResult<Statement> {
        let stmt = unsafe { ffi::mysql_stmt_init(self.0.as_ptr()) };
        // It is documented that the only reason `mysql_stmt_init` will fail
        // is because of OOM.
        // https://dev.mysql.com/doc/refman/5.7/en/mysql-stmt-init.html
        let stmt = NonNull::new(stmt).expect("Out of memory creating prepared statement");
        let stmt = Statement::new(stmt);
        stmt.prepare(query)?;
        Ok(stmt)
    }

    fn did_an_error_occur(&self) -> QueryResult<()> {
        use result::DatabaseErrorKind;
        use result::Error::DatabaseError;

        let error_message = self.last_error_message();
        if error_message.is_empty() {
            Ok(())
        } else {
            Err(DatabaseError(
                DatabaseErrorKind::__Unknown,
                Box::new(error_message),
            ))
        }
    }

    fn flush_pending_results(&self) -> QueryResult<()> {
        // We may have a result to process before advancing
        self.consume_current_result()?;
        while self.more_results() {
            self.next_result()?;
            self.consume_current_result()?;
        }
        Ok(())
    }

    fn consume_current_result(&self) -> QueryResult<()> {
        unsafe {
            let res = ffi::mysql_store_result(self.0.as_ptr());
            if !res.is_null() {
                ffi::mysql_free_result(res);
            }
        }
        self.did_an_error_occur()
    }

    fn more_results(&self) -> bool {
        unsafe { ffi::mysql_more_results(self.0.as_ptr()) != 0 }
    }

    fn next_result(&self) -> QueryResult<()> {
        unsafe { ffi::mysql_next_result(self.0.as_ptr()) };
        self.did_an_error_occur()
    }
}

impl Drop for RawConnection {
    fn drop(&mut self) {
        unsafe {
            ffi::mysql_close(self.0.as_ptr());
        }
    }
}

/// > In a non-multi-threaded environment, `mysql_init()` invokes
/// > `mysql_library_init()` automatically as necessary. However,
/// > `mysql_library_init()` is not thread-safe in a multi-threaded environment,
/// > and thus neither is `mysql_init()`. Before calling `mysql_init()`, either
/// > call `mysql_library_init()` prior to spawning any threads, or use a mutex
/// > to protect the `mysql_library_init()` call. This should be done prior to
/// > any other client library call.
///
/// <https://dev.mysql.com/doc/refman/5.7/en/mysql-init.html>
static MYSQL_THREAD_UNSAFE_INIT: Once = ONCE_INIT;

fn perform_thread_unsafe_library_initialization() {
    MYSQL_THREAD_UNSAFE_INIT.call_once(|| {
        // mysql_library_init is defined by `#define mysql_library_init mysql_server_init`
        // which isn't picked up by bindgen<commit_msg>Replace ONCE_INIT with Once::new()<commit_after>extern crate mysqlclient_sys as ffi;

use std::ffi::CStr;
use std::os::raw as libc;
use std::ptr::{self, NonNull};
use std::sync::Once;

use super::stmt::Statement;
use super::url::ConnectionOptions;
use result::{ConnectionError, ConnectionResult, QueryResult};

pub struct RawConnection(NonNull<ffi::MYSQL>);

impl RawConnection {
    pub fn new() -> Self {
        perform_thread_unsafe_library_initialization();
        let raw_connection = unsafe { ffi::mysql_init(ptr::null_mut()) };
        // We're trusting https://dev.mysql.com/doc/refman/5.7/en/mysql-init.html
        // that null return always means OOM
        let raw_connection =
            NonNull::new(raw_connection).expect("Insufficient memory to allocate connection");
        let result = RawConnection(raw_connection);

        // This is only non-zero for unrecognized options, which should never happen.
        let charset_result = unsafe {
            ffi::mysql_options(
                result.0.as_ptr(),
                ffi::mysql_option::MYSQL_SET_CHARSET_NAME,
                b"utf8mb4\0".as_ptr() as *const libc::c_void,
            )
        };
        assert_eq!(
            0, charset_result,
            "MYSQL_SET_CHARSET_NAME was not \
             recognized as an option by MySQL. This should never \
             happen."
        );

        result
    }

    pub fn connect(&self, connection_options: &ConnectionOptions) -> ConnectionResult<()> {
        let host = connection_options.host();
        let user = connection_options.user();
        let password = connection_options.password();
        let database = connection_options.database();
        let port = connection_options.port();

        unsafe {
            // Make sure you don't use the fake one!
            ffi::mysql_real_connect(
                self.0.as_ptr(),
                host.map(CStr::as_ptr).unwrap_or_else(|| ptr::null_mut()),
                user.as_ptr(),
                password
                    .map(CStr::as_ptr)
                    .unwrap_or_else(|| ptr::null_mut()),
                database
                    .map(CStr::as_ptr)
                    .unwrap_or_else(|| ptr::null_mut()),
                u32::from(port.unwrap_or(0)),
                ptr::null_mut(),
                0,
            )
        };

        let last_error_message = self.last_error_message();
        if last_error_message.is_empty() {
            Ok(())
        } else {
            Err(ConnectionError::BadConnection(last_error_message))
        }
    }

    pub fn last_error_message(&self) -> String {
        unsafe { CStr::from_ptr(ffi::mysql_error(self.0.as_ptr())) }
            .to_string_lossy()
            .into_owned()
    }

    pub fn execute(&self, query: &str) -> QueryResult<()> {
        unsafe {
            // Make sure you don't use the fake one!
            ffi::mysql_real_query(
                self.0.as_ptr(),
                query.as_ptr() as *const libc::c_char,
                query.len() as libc::c_ulong,
            );
        }
        self.did_an_error_occur()?;
        self.flush_pending_results()?;
        Ok(())
    }

    pub fn enable_multi_statements<T, F>(&self, f: F) -> QueryResult<T>
    where
        F: FnOnce() -> QueryResult<T>,
    {
        unsafe {
            ffi::mysql_set_server_option(
                self.0.as_ptr(),
                ffi::enum_mysql_set_option::MYSQL_OPTION_MULTI_STATEMENTS_ON,
            );
        }
        self.did_an_error_occur()?;

        let result = f();

        unsafe {
            ffi::mysql_set_server_option(
                self.0.as_ptr(),
                ffi::enum_mysql_set_option::MYSQL_OPTION_MULTI_STATEMENTS_OFF,
            );
        }
        self.did_an_error_occur()?;

        result
    }

    pub fn affected_rows(&self) -> usize {
        let affected_rows = unsafe { ffi::mysql_affected_rows(self.0.as_ptr()) };
        affected_rows as usize
    }

    pub fn prepare(&self, query: &str) -> QueryResult<Statement> {
        let stmt = unsafe { ffi::mysql_stmt_init(self.0.as_ptr()) };
        // It is documented that the only reason `mysql_stmt_init` will fail
        // is because of OOM.
        // https://dev.mysql.com/doc/refman/5.7/en/mysql-stmt-init.html
        let stmt = NonNull::new(stmt).expect("Out of memory creating prepared statement");
        let stmt = Statement::new(stmt);
        stmt.prepare(query)?;
        Ok(stmt)
    }

    fn did_an_error_occur(&self) -> QueryResult<()> {
        use result::DatabaseErrorKind;
        use result::Error::DatabaseError;

        let error_message = self.last_error_message();
        if error_message.is_empty() {
            Ok(())
        } else {
            Err(DatabaseError(
                DatabaseErrorKind::__Unknown,
                Box::new(error_message),
            ))
        }
    }

    fn flush_pending_results(&self) -> QueryResult<()> {
        // We may have a result to process before advancing
        self.consume_current_result()?;
        while self.more_results() {
            self.next_result()?;
            self.consume_current_result()?;
        }
        Ok(())
    }

    fn consume_current_result(&self) -> QueryResult<()> {
        unsafe {
            let res = ffi::mysql_store_result(self.0.as_ptr());
            if !res.is_null() {
                ffi::mysql_free_result(res);
            }
        }
        self.did_an_error_occur()
    }

    fn more_results(&self) -> bool {
        unsafe { ffi::mysql_more_results(self.0.as_ptr()) != 0 }
    }

    fn next_result(&self) -> QueryResult<()> {
        unsafe { ffi::mysql_next_result(self.0.as_ptr()) };
        self.did_an_error_occur()
    }
}

impl Drop for RawConnection {
    fn drop(&mut self) {
        unsafe {
            ffi::mysql_close(self.0.as_ptr());
        }
    }
}

/// > In a non-multi-threaded environment, `mysql_init()` invokes
/// > `mysql_library_init()` automatically as necessary. However,
/// > `mysql_library_init()` is not thread-safe in a multi-threaded environment,
/// > and thus neither is `mysql_init()`. Before calling `mysql_init()`, either
/// > call `mysql_library_init()` prior to spawning any threads, or use a mutex
/// > to protect the `mysql_library_init()` call. This should be done prior to
/// > any other client library call.
///
/// <https://dev.mysql.com/doc/refman/5.7/en/mysql-init.html>
static MYSQL_THREAD_UNSAFE_INIT: Once = Once::new();

fn perform_thread_unsafe_library_initialization() {
    MYSQL_THREAD_UNSAFE_INIT.call_once(|| {
        // mysql_library_init is defined by `#define mysql_library_init mysql_server_init`
        // which isn't picked up by bindgen
{'fraction_non_alphanumeric': 0.12421758569299553, 'fraction_numerical': 0.004023845007451565, 'mean_word_length': 2.06625542609093, 'pattern_counts': {'":': 0, '<': 27, '<?xml version=': 0, '>': 67, 'https://': 6, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1345744', 'n_tokens_mistral': 4112, 'n_tokens_neox': 4012, 'n_words': 805}