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 "] 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 = LazyLock::new(|| Regex::new(r"\p{Zs}++ /// ), /// ); /// ``` +/// +/// # Examples (`with-sea-orm`) +/// +/// See [`fuzzy_separated`] examples. pub fn prefix>(text: T) -> Keyword { Keyword { ty: KeywordType::Prefix, @@ -163,6 +172,10 @@ pub fn prefix>(text: T) -> Keyword { /// ), /// ); /// ``` +/// +/// # Examples (`with-sea-orm`) +/// +/// See [`fuzzy_separated`] examples. pub fn suffix>(text: T) -> Keyword { Keyword { ty: KeywordType::Suffix, @@ -229,6 +242,10 @@ pub fn suffix>(text: T) -> Keyword { /// ), /// ); /// ``` +/// +/// # Examples (`with-sea-orm`) +/// +/// See [`fuzzy_separated`] examples. pub fn fuzzy>(text: T) -> Keyword { Keyword { ty: KeywordType::Fuzzy, @@ -306,6 +323,83 @@ pub fn fuzzy>(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, +/// } +/// +/// #[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>(text: T) -> Keywords { keywords( SEPARATOR_REGEX @@ -382,11 +476,16 @@ pub fn fuzzy_separated>(text: T) -> Keywords { /// ), /// ); /// ``` +/// +/// # Examples (`with-sea-orm`) +/// +/// See [`fuzzy_separated`] examples. pub fn keywords, Iter: IntoIterator>(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 for Keyword { + #[inline] fn from(value: String) -> Self { fuzzy(value) } @@ -402,8 +502,9 @@ impl From 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(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(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(self, columns: Iter) -> Condition + where + C: ColumnTrait, + Iter: IntoIterator, + { + 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(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(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(self, columns: Iter) -> Condition + where + C: ColumnTrait, + Iter: IntoIterator, + { + self.into_condition_for_columns(columns.into_iter().map(|col| col.as_column_ref())) + } }