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")