From b9010d5e2f8e9f6b9996376250940f7e06a4c1d1 Mon Sep 17 00:00:00 2001
From: mpyw <ryosuke_i_628@yahoo.co.jp>
Date: Mon, 26 Aug 2024 01:18:28 +0900
Subject: [PATCH] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20Support=20`sea=5Form::Co?=
 =?UTF-8?q?lumnTrait`=20with=20fully-qualification?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .github/workflows/test.yaml |  82 +++++++++++++++++---
 Cargo.toml                  |  18 ++++-
 README.md                   |   2 +
 src/lib.rs                  | 151 +++++++++++++++++++++++++++++++++++-
 4 files changed, 239 insertions(+), 14 deletions(-)

diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
index 7eb9225..3939646 100644
--- a/.github/workflows/test.yaml
+++ b/.github/workflows/test.yaml
@@ -2,33 +2,87 @@ name: "Test"
 
 on: [push]
 
+defaults:
+  run:
+    shell: bash -euo pipefail {0}
+
 jobs:
   rust-test:
     timeout-minutes: 10
     runs-on: ubuntu-latest
     strategy:
       matrix:
-        sea-query: ["0.30", "0.31"]
-        sqlx: ["0.7", "0.8"]
+        sea:
+          - { sea-orm: "1.0", sea-query: "0.31" }
+        use-sea-orm:
+          - true
+          - false
+        sqlx:
+          - "0.7"
+          - "0.8"
+        exclude:
+          - sea: { sea-orm: "1.0", sea-query: "0.31" }
+            use-sea-orm: true
+            sqlx: "0.8"
     steps:
       - uses: actions/checkout@v4
       - name: Setup Rust stable toolchain
         uses: dtolnay/rust-toolchain@nightly
         with:
           components: rustfmt
+      - name: Install Coverage Tools
+        if: ${{ matrix.use-sea-orm }}
+        run: |
+          rustup component add llvm-tools-preview --toolchain nightly
+          cargo +nightly install cargo-llvm-cov
       - name: Update dependencies
         run: |
-          cargo add sea-query@${{ matrix.sea-query }}
-          cargo add --dev sqlx@${{ matrix.sqlx }}
-          cargo add --dev sqlx-postgres@${{ matrix.sqlx }}
+          cargo remove sea-orm sea-query
+          cargo remove --dev sea-orm sea-query sqlx sqlx-postgres
+          cargo add sea-query@${{ matrix.sea.sea-query }} --no-default-features
+          cargo add --dev sea-query@${{ matrix.sea.sea-query }}
+          if [ "${{ matrix.use-sea-orm }}" == "true" ]; then
+            cargo add sea-orm@${{ matrix.sea.sea-orm }} --no-default-features --optional
+            cargo add --dev sea-orm@${{ matrix.sea.sea-orm }} --features macros
+          fi
+          cargo add --dev sqlx@${{ matrix.sqlx }} sqlx-postgres@${{ matrix.sqlx }}
+          cat << EOF >> "$GITHUB_STEP_SUMMARY"
+          ## \`Cargo.toml\`
+          \`\`\`toml
+          $(cat Cargo.toml)
+          \`\`\`
+          EOF
+      - name: Generate feature flags
+        id: generate-flags
+        run: |
+          if [ "${{ matrix.use-sea-orm }}" == "true" ]; then
+            echo "feature_flags=--all-features" >> "$GITHUB_OUTPUT"
+          else
+            echo "feature_flags=" >> "$GITHUB_OUTPUT"
+          fi
       - name: Restore cache
         uses: Swatinem/rust-cache@v2
       - name: Check code (dependencies only)
-        run: cargo +nightly check
+        run: cargo +nightly check ${{ steps.generate-flags.outputs.feature_flags }}
       - name: Check code (with dev-dependencies)
-        run: cargo +nightly check --all-targets
+        run: cargo +nightly check --all-targets ${{ steps.generate-flags.outputs.feature_flags }}
       - name: Check test
-        run: cargo +nightly test
+        run: |
+          if [ "${{ matrix.use-sea-orm }}" == "true" ]; then
+            cargo +nightly llvm-cov ${{ steps.generate-flags.outputs.feature_flags }} --doctests --lcov >> /tmp/coverage.lcov
+          else
+            cargo test ${{ steps.generate-flags.outputs.feature_flags }}
+          fi
+      - name: Coveralls Parallel
+        if: ${{ matrix.use-sea-orm }}
+        uses: coverallsapp/github-action@v2
+        with:
+          files: /tmp/coverage.lcov
+          flag-name: >-
+            sea-orm:${{ matrix.sea.sea-orm }}
+            sea-query:${{ matrix.sea.sea-query }}
+            sqlx:${{ matrix.sqlx }}
+          parallel: true
 
   rust-lint:
     timeout-minutes: 10
@@ -48,6 +102,16 @@ jobs:
       - name: Check format
         run: cargo +nightly fmt --all -- --check
       - name: Check clippy
-        run: cargo clippy -- -D warnings
+        run: cargo clippy --all-features -- -D warnings
       - name: Check sort
         run: cargo sort -w -c
+
+  aggregate-coverages:
+    needs: rust-test
+    if: ${{ always() }}
+    runs-on: ubuntu-latest
+    steps:
+      - name: Coveralls Finished
+        uses: coverallsapp/github-action@v2
+        with:
+          parallel-finished: true
diff --git a/Cargo.toml b/Cargo.toml
index e2ac397..b064358 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "sea-query-common-like"
-version = "0.1.4"
+version = "1.0.0"
 authors = ["mpyw <mpyw628@gmail.com>"]
 edition = "2021"
 rust-version = "1.80.0"
@@ -8,16 +8,26 @@ description = "A Rust crate for enhancing sea_query with typical LIKE search sup
 repository = "https://github.com/yumemi-inc/sea-query-common-like"
 license = "MIT"
 include = ["/src", "LICENSE"]
-keywords = ["sea_query", "sql", "database", "LIKE", "search"]
+keywords = ["sea-orm", "sea-query", "sql", "LIKE", "search"]
 categories = ["database", "web-programming"]
 
 [dependencies]
 fancy-regex = { version = "0.13", default-features = false }
 regex = { version = "1", default-features = false, features = ["unicode-gencat"] }
-sea-query = { version = ">=0.30, <0.32", default-features = false }
+sea-orm = { version = ">=1.0, <1.1", default-features = false, optional = true }
+sea-query = { version = ">=0.31, <0.32", default-features = false }
 
 [dev-dependencies]
-sea-query = ">=0.30, <0.32"
+sea-orm = { version = ">=1.0, <1.1", features = ["macros"] }
+sea-query = ">=0.31, <0.32"
 sqlformat = "0.2.4"
 sqlx = ">=0.7, <0.9"
 sqlx-postgres = ">=0.7, <0.9"
+
+[features]
+default = []
+with-sea-orm = ["dep:sea-orm"]
+
+[package.metadata.docs.rs]
+all-features = true
+rustdoc-args = ["--cfg", "docsrs"]
diff --git a/README.md b/README.md
index bcb9b96..d57a8a5 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,7 @@
 # sea-query-common-like
 
+[![Coverage Status](https://coveralls.io/repos/github/yumemi-inc/sea-query-common-like/badge.svg?branch=support-sea-orm)](https://coveralls.io/github/yumemi-inc/sea-query-common-like?branch=support-sea-orm)
+
 A Rust crate for enhancing [`sea_query`](https://docs.rs/sea-query/latest/sea_query/) with typical `LIKE` search support, including escape sequences for patterns (`%fuzzy%`, `prefix%`, `%suffix`) and multi-column fuzzy search.
 
 - Documentation: [sea_query_common_like - Rust](https://docs.rs/sea-query-common-like/latest/sea_query_common_like/)
diff --git a/src/lib.rs b/src/lib.rs
index 0de5233..fc36854 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -4,11 +4,16 @@
 //! with typical `LIKE` search support, including escape sequences for patterns
 //! (`%fuzzy%`, `prefix%`, `%suffix`) and multi-column fuzzy search.
 
+#![cfg_attr(docsrs, feature(doc_cfg))]
+
 use fancy_regex::Regex as FancyRegex;
 use regex::Regex;
 use sea_query::{Cond, Condition, Expr, IntoColumnRef, IntoLikeExpr, LikeExpr};
 use std::sync::LazyLock;
 
+#[cfg(feature = "with-sea-orm")]
+use sea_orm::ColumnTrait;
+
 /// Represents a keyword used for `LIKE` search with different matching types.
 #[derive(Debug, Clone, Eq, PartialEq)]
 pub struct Keyword {
@@ -99,6 +104,10 @@ static SEPARATOR_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\p{Zs}++
 ///     ),
 /// );
 /// ```
+///
+/// # Examples (`with-sea-orm`)
+///
+/// See [`fuzzy_separated`] examples.
 pub fn prefix<T: Into<String>>(text: T) -> Keyword {
     Keyword {
         ty: KeywordType::Prefix,
@@ -163,6 +172,10 @@ pub fn prefix<T: Into<String>>(text: T) -> Keyword {
 ///     ),
 /// );
 /// ```
+///
+/// # Examples (`with-sea-orm`)
+///
+/// See [`fuzzy_separated`] examples.
 pub fn suffix<T: Into<String>>(text: T) -> Keyword {
     Keyword {
         ty: KeywordType::Suffix,
@@ -229,6 +242,10 @@ pub fn suffix<T: Into<String>>(text: T) -> Keyword {
 ///     ),
 /// );
 /// ```
+///
+/// # Examples (`with-sea-orm`)
+///
+/// See [`fuzzy_separated`] examples.
 pub fn fuzzy<T: Into<String>>(text: T) -> Keyword {
     Keyword {
         ty: KeywordType::Fuzzy,
@@ -306,6 +323,83 @@ pub fn fuzzy<T: Into<String>>(text: T) -> Keyword {
 ///     ),
 /// );
 /// ```
+///
+/// # Examples (`with-sea-orm`)
+///
+/// ```
+/// use sea_query::all;
+/// use sea_query_common_like::fuzzy_separated;
+/// use sqlformat::{format, FormatOptions, QueryParams};
+///
+/// #[cfg(feature = "with-sea-orm")]
+/// use sea_orm::{ColumnTrait, DbBackend, EntityTrait, QueryFilter, QueryTrait};
+///
+/// #[cfg(feature = "with-sea-orm")]
+/// mod book {
+///     use sea_orm::entity::prelude::*;
+///
+///     #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
+///     #[sea_orm(schema_name = "book", table_name = "books")]
+///     pub struct Model {
+///         #[sea_orm(primary_key, auto_increment = false)]
+///         pub id: Uuid,
+///         #[sea_orm(column_type = "Text")]
+///         pub title: String,
+///         #[sea_orm(column_type = "Text")]
+///         pub author: String,
+///         pub deleted_at: Option<DateTimeWithTimeZone>,
+///     }
+///
+///     #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
+///     pub enum Relation {
+///     }
+///
+///     impl ActiveModelBehavior for ActiveModel {}
+/// }
+///
+/// #[cfg(feature = "with-sea-orm")]
+/// assert_eq!(
+///     format(
+///         book::Entity::find()
+///             .filter(all![
+///                 fuzzy_separated("1% 99% Edison").into_condition_for_orm_columns([book::Column::Title, book::Column::Author]),
+///                 book::Column::DeletedAt.is_null(),
+///             ])
+///             .build(DbBackend::Postgres)
+///             .to_string()
+///             .as_str(),
+///         &QueryParams::default(),
+///         FormatOptions::default(),
+///     ),
+///     format(
+///         r#"
+///             SELECT
+///                 "books"."id",
+///                 "books"."title",
+///                 "books"."author",
+///                 "books"."deleted_at"
+///             FROM
+///                 "book"."books"
+///             WHERE
+///                 (
+///                     "books"."title" LIKE '%1!%%' ESCAPE '!'
+///                     OR "books"."author" LIKE '%1!%%' ESCAPE '!'
+///                 )
+///                 AND (
+///                     "books"."title" LIKE '%99!%%' ESCAPE '!'
+///                     OR "books"."author" LIKE '%99!%%' ESCAPE '!'
+///                 )
+///                 AND (
+///                     "books"."title" LIKE '%Edison%' ESCAPE '!'
+///                     OR "books"."author" LIKE '%Edison%' ESCAPE '!'
+///                 )
+///                 AND "books"."deleted_at" IS NULL
+///         "#,
+///         &QueryParams::default(),
+///         FormatOptions::default(),
+///     ),
+/// );
+/// ```
 pub fn fuzzy_separated<T: Into<String>>(text: T) -> Keywords {
     keywords(
         SEPARATOR_REGEX
@@ -382,11 +476,16 @@ pub fn fuzzy_separated<T: Into<String>>(text: T) -> Keywords {
 ///     ),
 /// );
 /// ```
+///
+/// # Examples (`with-sea-orm`)
+///
+/// See [`fuzzy_separated`] examples.
 pub fn keywords<T: Into<Keyword>, Iter: IntoIterator<Item = T>>(texts: Iter) -> Keywords {
     Keywords(texts.into_iter().map(Into::into).collect())
 }
 
 // Escape special characters in a `LIKE` search pattern.
+#[inline(always)]
 fn escape_like_value(input: &str) -> String {
     ESCAPE_REGEX
         .replace_all(input, ESCAPE_CHAR.to_string())
@@ -395,6 +494,7 @@ fn escape_like_value(input: &str) -> String {
 
 /// Default conversion from [`String`] to fuzzy [`Keyword`].
 impl From<String> for Keyword {
+    #[inline]
     fn from(value: String) -> Self {
         fuzzy(value)
     }
@@ -402,8 +502,9 @@ impl From<String> for Keyword {
 
 /// Default conversion from `&str` to fuzzy [`Keyword`].
 impl From<&str> for Keyword {
+    #[inline]
     fn from(value: &str) -> Self {
-        Self::from(value.to_string())
+        fuzzy(value.to_string())
     }
 }
 
@@ -422,6 +523,7 @@ impl IntoLikeExpr for Keyword {
 /// Methods for converting [`Keyword`] into [`sea_query::Condition`] for a single or multiple columns.
 impl Keyword {
     /// Generate a [`Condition`] for a single column with the `LIKE` pattern.
+    #[inline]
     pub fn into_condition_for_column<T: IntoColumnRef>(self, column: T) -> Condition {
         self.into_condition_for_columns([column])
     }
@@ -436,11 +538,35 @@ impl Keyword {
             .map(|col| Expr::col(col).like(self.clone()))
             .fold(Cond::all(), Cond::add)
     }
+
+    /// Generate a [`Condition`] for a single column with the `LIKE` pattern using a fully-qualified column name with `sea-orm`.
+    #[cfg(feature = "with-sea-orm")]
+    #[cfg_attr(docsrs, doc(cfg(feature = "with-sea-orm")))]
+    #[inline]
+    pub fn into_condition_for_orm_column<C>(self, column: C) -> Condition
+    where
+        C: ColumnTrait,
+    {
+        self.into_condition_for_orm_columns([column])
+    }
+
+    /// Generate a [`Condition`] for multiple columns with the `LIKE` pattern using fully-qualified column names with `sea-orm`.
+    #[cfg(feature = "with-sea-orm")]
+    #[cfg_attr(docsrs, doc(cfg(feature = "with-sea-orm")))]
+    #[inline]
+    pub fn into_condition_for_orm_columns<C, Iter>(self, columns: Iter) -> Condition
+    where
+        C: ColumnTrait,
+        Iter: IntoIterator<Item = C>,
+    {
+        self.into_condition_for_columns(columns.into_iter().map(|col| col.as_column_ref()))
+    }
 }
 
 /// Methods for converting [`Keywords`] into [`sea_query::Condition`] for a single or multiple columns.
 impl Keywords {
     /// Generate a [`Condition`] for a single column with multiple `LIKE` patterns.
+    #[inline]
     pub fn into_condition_for_column<T: IntoColumnRef + Clone>(self, column: T) -> Condition {
         self.into_condition_for_columns([column])
     }
@@ -461,4 +587,27 @@ impl Keywords {
             })
             .fold(Cond::all(), Cond::add)
     }
+
+    /// Generate a [`Condition`] for a single column with multiple `LIKE` patterns using a fully-qualified column name with `sea-orm`.
+    #[cfg(feature = "with-sea-orm")]
+    #[cfg_attr(docsrs, doc(cfg(feature = "with-sea-orm")))]
+    #[inline]
+    pub fn into_condition_for_orm_column<C>(self, column: C) -> Condition
+    where
+        C: ColumnTrait,
+    {
+        self.into_condition_for_orm_columns([column])
+    }
+
+    /// Generate a [`Condition`] for multiple columns with multiple `LIKE` patterns using fully-qualified column names with `sea-orm`.
+    #[cfg(feature = "with-sea-orm")]
+    #[cfg_attr(docsrs, doc(cfg(feature = "with-sea-orm")))]
+    #[inline]
+    pub fn into_condition_for_orm_columns<C, Iter>(self, columns: Iter) -> Condition
+    where
+        C: ColumnTrait,
+        Iter: IntoIterator<Item = C>,
+    {
+        self.into_condition_for_columns(columns.into_iter().map(|col| col.as_column_ref()))
+    }
 }