Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[11.x] Introduce where[Date,Time,Year,Month,Day]Between methods in query builder #53105

Open
wants to merge 21 commits into
base: 11.x
Choose a base branch
from

Conversation

malkhazidartsmelidze
Copy link

@malkhazidartsmelidze malkhazidartsmelidze commented Oct 10, 2024

Added following methods to Illuminate\Database\Query\Builder class:
whereDateBetween()
whereTimeBetween()
whereYearBetween()
whereMonthBetween()
whereDayBetween()
orWhereDateBetween()
orWhereTimeBetween()
orWhereYearBetween()
orWhereMonthBetween()
orWhereDayBetween()
whereDateNotBetween()
whereTimeNotBetween()
whereYearNotBetween()
whereMonthNotBetween()
whereDayNotBetween()

Eeach function works similar to existing whereBetween function and CarbonPeriod as well.

Usage examples:

// using DateTimeInterface
$orders = Order::whereDateBetween('created_at', [Carbon::parse('2024-01-01'), Carbon::parse('2024-12-31')])->count();

// using CarbonPeriod 
$orders = Order::whereYearBetween('created_at', now()->subYears(3)->toPeriod(now()->addYears(5)))->count(); 

// using Carbon\Month enum
$orders = Order::whereMonthBetween('created_at', [\Carbon\Month::January, \Carbon\Month::March])->count(); 

// using basic types
$articles = Post::whereYearBetween('published_at', [2022, 2026])->count(); 
  • PosgreSQL support
  • SQLite support
  • SqlServer support
  • Add orWhere[Date|Time|Year|Month|Day]Between support
  • Add where[Date|Time|Year|Month|Day]NotBetween support
  • Add orWhere[Date|Time|Year|Month|Day]NotBetween support
  • Currently date functions don't support indexing, Solve that using casting
  • More testing

I hope you'll find this pull request helpful.
Any suggestions are welcome

@shaedrich
Copy link

$articles = Post::whereMonthBetween('published_at', [1, 3])->get();

It would be nice, if this also supported enums out of the box:

$articles = Post::whereMonthBetween('published_at', [
    \Carbon\Month::January,
    \Carbon\Month::March,
])->get();
$articles = Post::whereDayBetween('published_at', [
    \Carbon\WeekDay::Monday,
    \Carbon\WeekDay::Wednesday,
])->get();

@devfrey
Copy link
Contributor

devfrey commented Oct 10, 2024

This has been attempted before: #42261

See this important comment regarding indexes when using functions like YEAR(): #42261 (comment)

@malkhazidartsmelidze
Copy link
Author

@devfrey

This has been attempted before: #42261

See this important comment regarding indexes when using functions like YEAR(): #42261 (comment)

I've seen that pull request and it lacks some functionality compared to this.
It only filters whereBetweenDate and doesn't include CarbonPeriod support as well.

Regarding to index, existing whereDate method doesn't use index to search on column.
Typically users add '00:00:00' and '23:59:59' to start and end date respectively but this approach can't work if timestamp has milisecond or microsecond accuracy. Same approach is used in #42261 pull request . I think that's a bit ugly too.

In my apps I have workaround using CAST() in MySQL and it works good. I can use index and can filter with microsecond accuracy at the same time.

Here is the snippet in case someone is interested: (Works only on MySQL)
<?php

use Carbon\CarbonPeriod;
use Illuminate\Database\Eloquent\Builder;

trait AdvancedDateFilters {
  /**
   * Filter timestamp/datetime column using casting
   *
   * @param Builder $query
   * @param string $column
   * @param DateTimeIterface|iterable<string|DateTimeIterface>|string $dates
   * @return void
   */
  public function scopeWhereTimestampDate(Builder $query, $column, $dates, $type = 'date') {
    if($dates instanceof CarbonPeriod) {
      $dates = [$dates->getStartDate(), $dates->getEndDate()];
    }

    if($dates instanceof DateTimeInterface) {
      $dates = [$dates];
    }
    
    $dates = Arr::wrap($dates);

    $query_interval = '';
    [$date_format, $date_interval] = match($type) {
      'date' => ['Y-m-d', 'day'],
      'year' => ['Y', 'year'],
    };

    $dates = array_slice($dates, 0, 2);
    
    // if only one date is provided, we will assume that it is the start and end date
    if(count($dates) == 1) {
      $dates[] = $dates[0];
      $query_interval = ' + interval 1 ' . $date_interval;
    }

    $dates = collect($dates)->map(function($date) use ($date_format) {
      return $date instanceof DateTimeInterface ? $date->format($date_format) : $date;
    })->all();

    $sql = sprintf("%s between cast(? as datetime) and cast(?%s as datetime)", $column, $query_interval);
    
    return $query->whereRaw($sql, $dates);
  }
}

I can integrate this in this pull request if interested

@malkhazidartsmelidze
Copy link
Author

It would be nice, if this also supported enums out of the box:

Definitelly can do this

@malkhazidartsmelidze
Copy link
Author

Added support for all default grammars.
Left this unchecked boxes:

  • Add orWhere[Date|Time|Year|Month|Day]Between support
  • Add whereNot[Date|Time|Year|Month|Day]Between support
  • Add orWhereNot[Date|Time|Year|Month|Day]Between support

Checking up this boxes brings up additional 15 functions in Illuminate\Database\Query\Builder class and about 200 lines of code.
Maybe it's time to ask if I should implement it or stick what we have now?
Asking because I see some unmerged PRs for the reason of code amount that ships with framework, which I find very reasonable.

- Currently date functions don't support indexing, Solve that using casting
I can improve this for Date and Year (currently only for mysql) . I can include this in this pull request as well (or create new one). Is it worth the effort or should I release package for it?

Thank you.

@shaedrich
Copy link

shaedrich commented Oct 13, 2024

Currently date functions don't support indexing

This could be solved with generated columns—to be convenient, these had to be auto-generated in a way preferably by using a trait, implementing an interface or a method on the Blueprint class—but I'm not sure if you want that 🤔

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('table_with_dates', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->dateTimeTz('activated_at')->supportsDateWheres();
            $table->timestamps();
        });
    }
}; 

@malkhazidartsmelidze
Copy link
Author

Just finished adding more tests and \Carbon\Month enum support for whereMonthBetween, orWhereMonthBetween and whereMonthNotBetween functions .

I don't think there's need for orWhere[Date|Time|Year|Month|Day]NotBetween functions, so decided not to add these functions.

I think at this state this pull request is ready for review and I'm ready for suggestions.

@malkhazidartsmelidze
Copy link
Author

@taylorotwell , please review and give me feedback if there's a need to add or change something.

@shaedrich
Copy link

It only filters whereBetweenDate and doesn't include CarbonPeriod support as well.

Since you now support CarbonPeriod, you could also support CarbonInterval—not sure if that is good idea, but I thought, I just throw that in

@malkhazidartsmelidze malkhazidartsmelidze marked this pull request as draft October 16, 2024 11:19
@malkhazidartsmelidze malkhazidartsmelidze marked this pull request as ready for review October 16, 2024 11:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants