FCO / Red

A WiP ORM for Raku

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Add support for "locking clause" on SELECT

jonathanstowe opened this issue · comments

Most modern RDBMS (e.g. Pg, Oracle, MySQL ,) support at least to some extent SELECT .... FOR UPDATE (with either NOWAIT or SKIP LOCKED qualifiers,) which is really useful in some applications (e.g. SKIP LOCKED is helpful for a queue with multiple readers.)

I guess this would be applied as a method on a resultset, adding to a new attribute on the AST::Select which then may be added to the generated SQL as required.

BTW this is related to https://twitter.com/gellyfish/status/1587016407300063232 - I hadn't quite grokked the SKIP LOCKED when I started thinking about it.

I've been wondering... would that make sense to all sub selects inside update or delete to be FOR UPDATE?

➜  Red git:(master) ✗ raku -I. -MRed -e '

model Bla { has $.id is serial; has $.value is column }

red-defaults default => database "Pg";
schema(Bla).drop.create;
my $*RED-DEBUG = True;

.say for Bla.^all.grep({ .id in Bla.^all.grep({ .id < 10 }).map: *.id })

'
SQL : SELECT
   "bla".id , "bla".value
FROM
   "bla"
WHERE
   "bla".id IN ( SELECT
      "bla".id as "data_1"
   FROM
      "bla"
   WHERE
      "bla".id < 10 )
BIND: []
➜  Red git:(master) ✗ raku -I. -MRed -e '

model Bla { has $.id is serial; has $.value is column }

red-defaults default => database "Pg";
schema(Bla).drop.create;
my $*RED-DEBUG = True;

Bla.^all.grep({ .id in Bla.^all.grep({ .id < 10 }).map(*.id) }).delete

'
SQL : DELETE FROM bla
WHERE "bla".id IN ( SELECT
   "bla".id as "data_1"
FROM
   "bla"
WHERE
   "bla".id < 10
FOR UPDATE )
BIND: []
➜  Red git:(master) ✗ raku -I. -MRed -e '

model Bla { has $.id is serial; has $.value is column }

red-defaults default => database "Pg";
schema(Bla).drop.create;
my $*RED-DEBUG = True;

Bla.^all.grep({ .id in Bla.^all.grep({ .id < 10 }).map(*.id).skip-locked }).delete

'
SQL : DELETE FROM bla
WHERE "bla".id IN ( SELECT
   "bla".id as "data_1"
FROM
   "bla"
WHERE
   "bla".id < 10
FOR UPDATE SKIP LOCKED )
BIND: []

I think to make it completely useful, we should implement RETURNING to delete...

Now I think that makes more sense:

➜  Red git:(master) ✗ raku -I. -MRed -e '

model Bla { has $.id is serial; has $.value is column }

red-defaults default => database "Pg";
schema(Bla).drop.create; Bla.^create(:value(rand)) xx 20;
my $*RED-DEBUG = True;

.say for Bla.^all.grep({ .id in Bla.^all.grep({ .id < 10 }).map(*.id).skip-locked.head: 1 }).delete

'
SQL : DELETE FROM bla
WHERE "bla".id IN ( SELECT
   "bla".id as "data_1"
FROM
   "bla"
WHERE
   "bla".id < 10
LIMIT 1
FOR UPDATE SKIP LOCKED )
RETURNING *
BIND: []
Bla.new(id => 1, value => "0.5834069415209749")

Is this pushed to github? If so I'll try out my sketch in the morning.

Nice one BTW I was going to have a hack when I have a few days off.

Yes,that's published here. Tomorrow I'll see what happened with the ecosystem test (but doesn't seem to be related to the change) and probably release that (if you don't find any problem on it)

Sorry for all the delay... I'm not having much time these days...

Yep that's fine and works as expected.

The test code was:

use Red;
use Red::Database;
use Red::Do;


model Job { ... }

model JobType is table('job_type') {
    has Int $.id is id is serial;
    has Str $.name  is column;
    has Str $.owned-by  is column(:nullable) is rw;
    has DateTime $.created  is column = DateTime.now;
    has          @.jobs     is relationship( { .job-type-id }, model => 'Job');
}

model Job is table('job') {
    has Int         $.id        is id is serial;
    has DateTime    $.created   is column = DateTime.now;
    has Bool        $.completed is column is rw = False;
    has DateTime    $.completed-at  is column is rw;
    has Int         $.job-type-id   is referencing(model => 'JobType', column => 'id');
    has             $.job-type is relationship({ .job-type-id }, model => 'JobType', :!optional);
}

my $GLOBAL::RED-DB = database 'Pg', dbname => 'red_test';
my $*RED-DEBUG = True;

my $j1 = JobType.^create( name => 'Job One' );
my $j2 = JobType.^create( name => 'Job Two' );

react {
    whenever $*RED-DB.dbh.listen('job') -> $ {
        red-do {
            for Job.^rs.grep(-> $v { !$v.completed && !$v.job-type.owned-by.defined }).skip-locked -> $job {
                sleep (0.2 .. 1.2).rand;
                say $job;
                $job.completed = True;
                $job.completed-at = DateTime.now;
                $job.^save;
            }
            True;
        }, :transaction;
    }
    whenever Supply.interval(0.5) -> $ {
        my $jt = ($j1, $j2).pick;
        $jt.jobs.create;
    }
}

It obviously needs a trigger to work, so:

create function new_job() returns trigger language plpgsql as $$
begin
perform pg_notify('job', '' || NEW.id || '' );
return NEW;
END
$$;
create trigger job_trigger after insert on job for each row execute procedure new_job();

A thing to note is that if there is a join in the statement being locked for in Pg, it must be an inner join (hence the :!optional on the relationship, ) if it isn't there will be a nasty message from Pg.

Sorry for all the delay... I'm not having much time these days...

Hey I haven't had to time to get around to the thing I wanted it for either.

I think update should also return nieces, as now does delete...

I still think we should integrate .dbh.listen with Red.events somehow... but I don't see a way that would make sense...

➜  Red git:(master) ✗ raku -I. -MRed -e '

model Bla { has $.id is serial; has $.value is rw is column }

red-defaults default => database "Pg";
schema(Bla).drop.create; Bla.^create(:value(rand)) xx 20;
my $*RED-DEBUG = True;

.say for Bla.^all.grep({ .id in Bla.^all.grep({ .id < 10 }).map(*.id).skip-locked.head: 1 }).map({ .value = 42 }).save

'
SQL : UPDATE bla SET
   value = $1
WHERE "bla".id IN ( SELECT
   "bla".id as "data_1"
FROM
   "bla"
WHERE
   "bla".id < 10
LIMIT 1
FOR UPDATE SKIP LOCKED )
RETURNING *

BIND: [42]
Bla.new(id => 1, value => "42")