From 80b3583710ec1313055670661d38882dee342634 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20St=C3=BChrmann?= Date: Fri, 11 Apr 2025 15:22:37 +0200 Subject: [PATCH 1/2] Add support for MySQL 8.0.1+ locking features This commit enhances the MySQL SQLMaker to support the new locking features introduced in MySQL 8.0.1: - Add support for the new standard SQL-compliant 'FOR SHARE' syntax (while maintaining backward compatibility with 'LOCK IN SHARE MODE') - Add support for 'NOWAIT' modifier to fail immediately on locked rows - Add support for 'SKIP LOCKED' modifier to skip locked rows - Add support for 'OF table_name' clause to lock specific tables The implementation allows for both simple string syntax for basic locking and a more flexible hash-based configuration for advanced locking options. Example usage: - Basic: {for => 'share'} - Advanced: {for => {type => 'update', modifier => 'nowait', of => ['table1']}} A deprecation warning is added for the legacy 'shared' option to encourage migration to the new standard 'share' option. --- lib/DBIx/Class/SQLMaker/MySQL.pm | 65 ++++++++++++++++++++++++++++---- 1 file changed, 57 insertions(+), 8 deletions(-) diff --git a/lib/DBIx/Class/SQLMaker/MySQL.pm b/lib/DBIx/Class/SQLMaker/MySQL.pm index 34ee05445..703b34804 100644 --- a/lib/DBIx/Class/SQLMaker/MySQL.pm +++ b/lib/DBIx/Class/SQLMaker/MySQL.pm @@ -97,19 +97,68 @@ sub delete { return ($sql, @bind); } -# LOCK IN SHARE MODE -my $for_syntax = { - update => 'FOR UPDATE', - shared => 'LOCK IN SHARE MODE' +# +# Support for MySQL lock clause syntax according to specification +# including updates introduced in MySQL 8.0.1) +# FOR UPDATE | FOR SHARE [OF tbl_name [, tbl_name] ...] [NOWAIT | SKIP LOCKED] +# + +my $lock_types = { + update => 'FOR UPDATE', + share => 'FOR SHARE', + shared => 'LOCK IN SHARE MODE' # Deprecated but maintained +}; + +my $lock_modifiers = { + nowait => 'NOWAIT', + skip_locked => 'SKIP LOCKED' }; sub _lock_select { - my ($self, $type) = @_; + my ($self, $type) = @_; - my $sql = $for_syntax->{$type} - || $self->throw_exception("Unknown SELECT .. FOR type '$type' requested"); + if ($type eq 'shared') { + warnings::warnif( + 'deprecated', + "'for => 'shared'' is deprecated. Please use 'for => 'share'' instead" + ); + } + + # Handle hash-based configuration to support new featureset + if (ref $type eq 'HASH') { + my $lock_type = $type->{type}; + my $tables = $type->{of}; + my $modifier = $type->{modifier}; + + my $lock_clause = $lock_types->{$lock_type} + || $self->throw_exception("Unknown SELECT .. FOR type '$lock_type' requested"); + + # Add OF clause if tables are specified + if ($tables) { + my @table_list = ref $tables eq 'ARRAY' ? @$tables : ($tables); + if (@table_list) { + my $quoted_tables = join(', ', + map { $self->_quote($_) } @table_list + ); + $lock_clause .= " OF $quoted_tables"; + } + } + + # Add modifier if specified + if ($modifier) { + my $mod_sql = $lock_modifiers->{$modifier} + || $self->throw_exception("Unknown lock modifier '$modifier' requested"); + $lock_clause .= " $mod_sql"; + } + + return " $lock_clause"; + } - return " $sql"; + # Handle simple string types (for backward compatibility) + my $sql = $lock_types->{$type} + || $self->throw_exception("Unknown SELECT .. FOR type '$type' requested"); + + return " $sql"; } 1; From 70afdd53e76cd8f436d9616699f91821e7776ac3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20St=C3=BChrmann?= Date: Fri, 11 Apr 2025 15:51:24 +0200 Subject: [PATCH 2/2] Add comprehensive test coverage for MySQL locking features This commit adds test coverage for both existing and new MySQL locking features: Existing features: - Basic locking with 'for => "update"' - Legacy shared locking with 'for => "shared"' (with warning suppression) New features (MySQL 8.0.1+): - Standard SQL-compliant shared locking with 'for => "share"' - Hash-based configuration for advanced locking options: - NOWAIT modifier to fail immediately on locked rows - SKIP LOCKED modifier to skip locked rows - OF clause to lock specific tables - Combinations of modifiers and table specifications The tests verify that the SQL generated for each locking option is correct, ensuring that the implementation works as expected across all supported locking features. --- t/sqlmaker/mysql.t | 165 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 165 insertions(+) diff --git a/t/sqlmaker/mysql.t b/t/sqlmaker/mysql.t index 0e2ad2961..b292a9825 100644 --- a/t/sqlmaker/mysql.t +++ b/t/sqlmaker/mysql.t @@ -152,4 +152,169 @@ for ( ); } +# Test support for locking clauses +{ + # Test basic locking options + is_same_sql_bind( + $schema->resultset('Artist')->search({}, {for => 'update'})->as_query, + '( + SELECT `me`.`artistid`, `me`.`name`, `me`.`rank`, `me`.`charfield` + FROM `artist` `me` + FOR UPDATE + )', + [], + 'FOR UPDATE lock works correctly' + ); + + is_same_sql_bind( + $schema->resultset('Artist')->search({}, {for => 'shared'})->as_query, + '( + SELECT `me`.`artistid`, `me`.`name`, `me`.`rank`, `me`.`charfield` + FROM `artist` `me` + LOCK IN SHARE MODE + )', + [], + 'LOCK IN SHARE MODE (deprecated shared) works correctly' + ); + + is_same_sql_bind( + $schema->resultset('Artist')->search({}, {for => 'share'})->as_query, + '( + SELECT `me`.`artistid`, `me`.`name`, `me`.`rank`, `me`.`charfield` + FROM `artist` `me` + FOR SHARE + )', + [], + 'FOR SHARE (new share syntax) works correctly' + ); + + # Test hash-based locking configuration + is_same_sql_bind( + $schema->resultset('Artist')->search({}, {for => {type => 'update'}})->as_query, + '( + SELECT `me`.`artistid`, `me`.`name`, `me`.`rank`, `me`.`charfield` + FROM `artist` `me` + FOR UPDATE + )', + [], + 'Hash-based FOR UPDATE works correctly' + ); + + is_same_sql_bind( + $schema->resultset('Artist')->search({}, {for => {type => 'share'}})->as_query, + '( + SELECT `me`.`artistid`, `me`.`name`, `me`.`rank`, `me`.`charfield` + FROM `artist` `me` + FOR SHARE + )', + [], + 'Hash-based FOR SHARE works correctly' + ); + + # Test NOWAIT modifier + is_same_sql_bind( + $schema->resultset('Artist')->search({}, {for => {type => 'update', modifier => 'nowait'}})->as_query, + '( + SELECT `me`.`artistid`, `me`.`name`, `me`.`rank`, `me`.`charfield` + FROM `artist` `me` + FOR UPDATE NOWAIT + )', + [], + 'FOR UPDATE NOWAIT works correctly' + ); + + is_same_sql_bind( + $schema->resultset('Artist')->search({}, {for => {type => 'share', modifier => 'nowait'}})->as_query, + '( + SELECT `me`.`artistid`, `me`.`name`, `me`.`rank`, `me`.`charfield` + FROM `artist` `me` + FOR SHARE NOWAIT + )', + [], + 'FOR SHARE NOWAIT works correctly' + ); + + # Test SKIP LOCKED modifier + is_same_sql_bind( + $schema->resultset('Artist')->search({}, {for => {type => 'update', modifier => 'skip_locked'}})->as_query, + '( + SELECT `me`.`artistid`, `me`.`name`, `me`.`rank`, `me`.`charfield` + FROM `artist` `me` + FOR UPDATE SKIP LOCKED + )', + [], + 'FOR UPDATE SKIP LOCKED works correctly' + ); + + is_same_sql_bind( + $schema->resultset('Artist')->search({}, {for => {type => 'share', modifier => 'skip_locked'}})->as_query, + '( + SELECT `me`.`artistid`, `me`.`name`, `me`.`rank`, `me`.`charfield` + FROM `artist` `me` + FOR SHARE SKIP LOCKED + )', + [], + 'FOR SHARE SKIP LOCKED works correctly' + ); + + # Test OF clause + is_same_sql_bind( + $schema->resultset('Artist')->search({}, {for => {type => 'update', of => 'artist'}})->as_query, + '( + SELECT `me`.`artistid`, `me`.`name`, `me`.`rank`, `me`.`charfield` + FROM `artist` `me` + FOR UPDATE OF `artist` + )', + [], + 'FOR UPDATE OF table works correctly' + ); + + # Test OF clause with multiple tables + is_same_sql_bind( + $schema->resultset('Artist')->search({}, {for => {type => 'share', of => ['artist', 'cd']}})->as_query, + '( + SELECT `me`.`artistid`, `me`.`name`, `me`.`rank`, `me`.`charfield` + FROM `artist` `me` + FOR SHARE OF `artist`, `cd` + )', + [], + 'FOR SHARE OF multiple tables works correctly' + ); + + # Test combination of OF clause and modifier + is_same_sql_bind( + $schema->resultset('Artist')->search({}, { + for => { + type => 'update', + of => 'artist', + modifier => 'nowait' + } + })->as_query, + '( + SELECT `me`.`artistid`, `me`.`name`, `me`.`rank`, `me`.`charfield` + FROM `artist` `me` + FOR UPDATE OF `artist` NOWAIT + )', + [], + 'FOR UPDATE OF table NOWAIT works correctly' + ); + + is_same_sql_bind( + $schema->resultset('Artist')->search({}, { + for => { + type => 'share', + of => ['artist', 'cd'], + modifier => 'skip_locked' + } + })->as_query, + '( + SELECT `me`.`artistid`, `me`.`name`, `me`.`rank`, `me`.`charfield` + FROM `artist` `me` + FOR SHARE OF `artist`, `cd` SKIP LOCKED + )', + [], + 'FOR SHARE OF multiple tables SKIP LOCKED works correctly' + ); +} + done_testing;