File Coverage

lib/DB/Object/Fields/Field.pm
Criterion Covered Total %
statement 110 167 65.8
branch 27 60 45.0
condition 16 35 45.7
subroutine 42 66 63.6
pod 19 19 100.0
total 214 347 61.6


line stmt bran cond sub pod time code
1             ##----------------------------------------------------------------------------
2             ## Database Object Interface - ~/lib/DB/Object/Fields/Field.pm
3             ## Version v1.0.2
4             ## Copyright(c) 2021 DEGUEST Pte. Ltd.
5             ## Author: Jacques Deguest <jack@deguest.jp>
6             ## Created 2020/01/01
7             ## Modified 2023/06/12
8             ## All rights reserved
9             ##
10             ## This program is free software; you can redistribute it and/or modify it
11             ## under the same terms as Perl itself.
12             ##----------------------------------------------------------------------------
13             package DB::Object::Fields::Field;
14             BEGIN
15             {
16 2     2   110671 use strict;
  2         14  
  2         60  
17 2     2   21 use warnings;
  2         2  
  2         49  
18 2     2   577 use common::sense;
  2         15  
  2         13  
19 2     2   630 use parent qw( Module::Generic );
  2         312  
  2         10  
20 2     2   12464315 use vars qw( $VERSION );
  2         6  
  2         86  
21 2     2   581 use Devel::Confess;
  2         7760  
  2         12  
22 2     2   887 use Module::Generic::Array;
  2         10487  
  2         1494  
23             use overload (
24             '""' => 'as_string',
25 2     2   30 'bool' => sub{1},
26 2     2   21 '+' => sub{ &_op_overload( @_, '+' ) },
27 1     1   15 '-' => sub{ &_op_overload( @_, '-' ) },
28 1     1   16 '*' => sub{ &_op_overload( @_, '*' ) },
29 1     1   29 '/' => sub{ &_op_overload( @_, '/' ) },
30 1     1   15 '%' => sub{ &_op_overload( @_, '%' ) },
31 1     1   17 '<' => sub{ &_op_overload( @_, '<' ) },
32 1     1   2363 '>' => sub{ &_op_overload( @_, '>' ) },
33 1     1   19 '<=' => sub{ &_op_overload( @_, '<=' ) },
34 1     1   11 '>=' => sub{ &_op_overload( @_, '>=' ) },
35 1     1   12 '!=' => sub{ &_op_overload( @_, '<>' ) },
36 3     3   29 '<<' => sub{ &_op_overload( @_, '<<' ) },
37 1     1   6 '>>' => sub{ &_op_overload( @_, '>>' ) },
38 0     0   0 'lt' => sub{ &_op_overload( @_, '<' ) },
39 0     0   0 'gt' => sub{ &_op_overload( @_, '>' ) },
40 0     0   0 'le' => sub{ &_op_overload( @_, '<=' ) },
41 0     0   0 'ge' => sub{ &_op_overload( @_, '>=' ) },
42 0     0   0 'ne' => sub{ &_op_overload( @_, '<>' ) },
43 1     1   11 '&' => sub{ &_op_overload( @_, '&' ) },
44 1     1   6 '^' => sub{ &_op_overload( @_, '^' ) },
45 1     1   12 '|' => sub{ &_op_overload( @_, '|' ) },
46 1     1   18 '==' => sub{ &_op_overload( @_, '=' ) },
47 0     0   0 'eq' => sub{ &_op_overload( @_, '=' ) },
48             # Full Text Search operator
49 0     0   0 '~~' => sub{ &_op_overload( @_, '@@' ) },
50 2         82 fallback => 1,
51 2     2   15 );
  2         10  
52 2     2   719 use Want;
  2         3  
  2         144  
53 2     2   41 our( $VERSION ) = 'v1.0.2';
54             };
55              
56 2     2   12 use strict;
  2         3  
  2         56  
57 2     2   11 use warnings;
  2         5  
  2         723  
58              
59             sub init
60             {
61 1     1 1 211 my $self = shift( @_ );
62 1         135 $self->{default} = '';
63 1         16 $self->{name} = '';
64 1         9 $self->{pos} = '';
65 1         12 $self->{prefixed} = 0;
66 1         13 $self->{query_object} = '';
67 1         8 $self->{table_object} = '';
68 1         10 $self->{type} = '';
69 1         21 $self->{_init_params_order} = [qw( table_object query_object default pos type prefixed name )];
70 1         13 $self->{_init_strict_use_sub} = 1;
71 1 50       30 $self->SUPER::init( @_ ) || return( $self->pass_error );
72 1 50       15 return( $self->error( "No table object was provided." ) ) if( !$self->{table_object} );
73 1 50       19 return( $self->error( "Table object provided is not an object." ) ) if( !$self->_is_object( $self->{table_object} ) );
74 1 50       36 return( $self->error( "Table object provided is not a DB::Object::Tables object." ) ) if( !$self->{table_object}->isa( 'DB::Object::Tables' ) );
75 1 50       18 return( $self->error( "No name was provided for this field." ) ) if( !$self->{name} );
76 1         16 $self->{trace} = $self->_get_stack_trace;
77 1         317 return( $self );
78             }
79              
80 0     0 1 0 sub as_string { return( shift->name ); }
81              
82             # A data type constant
83 16     16 1 463 sub constant { return( shift->_set_get_hash_as_object( 'constant', @_ ) ); }
84              
85 0     0 1 0 sub database { return( shift->database_object->database ); }
86              
87 71     71 1 654 sub database_object { return( shift->table_object->database_object ); }
88              
89 0     0 1 0 sub default { return( shift->_set_get_scalar( 'default', @_ ) ); }
90              
91 0     0 1 0 sub first { return( shift->_find_siblings( 1 ) ); }
92              
93             sub last
94             {
95 0     0 1 0 my $self = shift( @_ );
96 0         0 my $fields = $self->table_object->fields;
97 0         0 my $pos = scalar( keys( %$fields ) );
98 0         0 return( $self->_find_siblings( $pos ) );
99             }
100              
101             sub name
102             {
103 20     20 1 98 my $self = shift( @_ );
104 2     2   14 no overloading;
  2         5  
  2         1929  
105 20 100       57 if( @_ )
106             {
107 1         8 $self->{name} = shift( @_ );
108             }
109 20         44 my $name = $self->{name};
110 20         70 my $trace = $self->_get_stack_trace;
111 20         6200 my $alias = $self->query_object->table_alias;
112 20 50       18086 if( $self->{prefixed} )
113             {
114 0         0 my @prefix = ();
115 0 0       0 if( length( $alias ) )
116             {
117 0         0 CORE::push( @prefix, $alias );
118             }
119             else
120             {
121             # if the value is higher than 1, we also add the database name as a prefix
122             # For example $tbl->fields->some_field->prefixed(2)->name
123 0 0       0 push( @prefix, $self->database ) if( $self->{prefixed} > 2 );
124 0 0 0     0 push( @prefix, $self->table_object->schema ) if( $self->{prefixed} > 1 && CORE::length( $self->table_object->schema ) );
125 0         0 push( @prefix, $self->table );
126             }
127 0         0 push( @prefix, $name );
128 0         0 return( join( '.', @prefix ) );
129             }
130             else
131             {
132 20         227 return( $name );
133             }
134             }
135              
136             sub next
137             {
138 0     0 1 0 my $self = shift( @_ );
139 0         0 return( $self->_find_siblings( $self->pos + 1 ) );
140             }
141              
142 0     0 1 0 sub pos { return( shift->_set_get_scalar( 'pos', @_ ) ); }
143              
144             sub prefixed
145             {
146 0     0 1 0 my $self = shift( @_ );
147 0 0       0 if( @_ )
148             {
149 0 0       0 $self->{prefixed} = ( $_[0] =~ /^\d+$/ ? $_[0] : ( $_[0] ? 1 : 0 ) );
    0          
150             }
151             else
152             {
153 0         0 $self->{prefixed} = 1;
154             }
155 0 0       0 return( want( 'OBJECT' ) ? $self : $self->{prefixed} );
156             }
157              
158             sub prev
159             {
160 0     0 1 0 my $self = shift( @_ );
161 0         0 return( $self->_find_siblings( $self->pos - 1 ) );
162             }
163              
164 21     21 1 142 sub query_object { return( shift->_set_get_object_without_init( 'query_object', 'DB::Object::Query', @_ ) ); }
165             # sub query_object { return( shift->table_object->query_object ); }
166              
167 0     0 1 0 sub schema { return( shift->table_object->schema ); }
168              
169 0     0 1 0 sub table { return( shift->table_object->name ); }
170              
171 0     0 1 0 sub table_name { return( shift->table_object->name ); }
172              
173 72     72 1 446 sub table_object { return( shift->_set_get_object_without_init( 'table_object', 'DB::Object::Tables', @_ ) ); }
174              
175 0     0 1 0 sub type { return( shift->_set_get_scalar( 'type', @_ ) ); }
176              
177             sub _find_siblings
178             {
179 0     0   0 my $self = shift( @_ );
180 0         0 my $pos = shift( @_ );
181 0 0       0 return( $self->error( "No field position provided." ) ) if( !CORE::length( $pos ) );
182 0 0       0 return if( $pos < 0 );
183 0         0 my $fields = $self->table_object->fields;
184 0         0 my $next_field;
185 0         0 foreach my $f ( sort{ $fields->{ $a } <=> $fields->{ $b } } keys( %$fields ) )
  0         0  
186             {
187 0 0       0 if( $fields->{ $f } == $pos )
188             {
189 0         0 $next_field = $f;
190 0         0 CORE::last;
191             }
192             }
193 0 0       0 return if( !defined( $next_field ) );
194 0   0     0 my $o = $self->table_object->fields_object->_initiate_field_object( $next_field ) ||
195             return( $self->pass_error( $self->table_object->fields_object->error ) );
196 0         0 return( $o );
197             }
198              
199             # Ref:
200             # <https://www.postgresql.org/docs/10/functions-comparison.html>
201             # <https://www.postgresql.org/docs/10/functions-math.html>
202             # <https://dev.mysql.com/doc/refman/5.7/en/comparison-operators.html>
203             # <https://sqlite.org/lang_expr.html>
204             sub _op_overload
205             {
206 19     19   167 my( $self, $val, $swap, $op ) = @_;
207 19 50       113 if( $self->_is_a( $val => 'DB::Object::IN' ) )
208             {
209 0         0 return( $val->_opt_overload( $self, 1, $op ) );
210             }
211            
212             # print( STDERR ref( $self ), "::_op_overload: Parameters provided are: '", join( "', '", @_ ), "'\n" );
213 19         385 my $field = $self->name;
214 19         153 my $map =
215             {
216             '!=' => '<>',
217             'lt' => '<',
218             'gt' => '>',
219             'le' => '<=',
220             'ge' => '>=',
221             # '=' works for all types, but IS does not work with everything.
222             # For example:
223             # select * from ip_table where ip_addr IS inet '192.168.2.12' OR inet '192.168.2.12' << ip_addr
224             # does not work, but
225             # select * from ip_table where ip_addr = inet '192.168.2.12' OR inet '192.168.2.12' << ip_addr
226             # works better
227             '==' => '=',
228             };
229 19 50       70 $op = $map->{ $op } if( exists( $map->{ $op } ) );
230 19 100 66     78 $op = 'IS' if( $op eq '=' and $val eq 'NULL' );
231             # If the value specified in the operation is a placeholder, or a field object or a statement object, we do not want to quote process it
232 19 100 0     105 unless( $val eq '?' ||
      33        
      33        
      66        
      100        
      100        
233             ( $self->_is_object( $val ) &&
234             (
235             $val->isa( 'DB::Object::Fields::Field' ) ||
236             $val->isa( 'DB::Object::Statement' )
237             )
238             ) ||
239             $self->database_object->placeholder->has( \$val ) ||
240             $self->_is_scalar( $val ) ||
241             uc( $val ) eq 'NULL' )
242             {
243 16 50       196 $val = $self->database_object->quote( $val, $self->constant->constant ) if( $self->database_object );
244             }
245            
246 19         195 my $types;
247             # If the value is a statement object, stringify it, surround it with parenthesis and use it
248 19 100       78 if( $self->_is_a( $val, 'DB::Object::Statement' ) )
    50          
    100          
    50          
    100          
249             {
250 0         0 $val = '(' . $val->as_string . ')';
251             }
252             elsif( $self->database_object->placeholder->has( $self->_is_scalar( $val ) ? $val : \$val ) )
253             {
254 1         12 $types = $self->database_object->placeholder->replace( \$val );
255             }
256             # A placeholder, but don't know the type
257             elsif( $val eq '?' )
258             {
259 0         0 $types = Module::Generic::Array->new( [''] );
260             }
261             elsif( $self->_is_scalar( $val ) )
262             {
263 1         9 $val = $$val;
264             }
265 19 100 66     291 return( DB::Object::Fields::Field::Overloaded->new(
    100          
266             expression =>
267             (
268             $swap
269             ? "${val} ${op} ${field}"
270             : "${field} ${op} ${val}"
271             ),
272             field => $self,
273             binded => ( $val eq '?' || $types ) ? 1 : 0,
274             # types => $types,
275             ) );
276             }
277              
278             {
279             # NOTE: package DB::Object::Fields::Field::Overloaded
280             # The purpose of this package is to tag overloaded operation so we can handle them properly later
281             # such as in a where clause
282             package
283             DB::Object::Fields::Field::Overloaded;
284 2     2   17 use strict;
  2         4  
  2         45  
285 2     2   13 use common::sense;
  2         4  
  2         10  
286             use overload (
287 19     19   7490 '""' => sub{ return( $_[0]->{expression} ) },
288 2         16 fallback => 1,
289 2     2   189 );
  2         6  
290             our $VERSION = 'v0.1.0';
291              
292             sub new
293             {
294 19     19   33 my $this = shift( @_ );
295             # This contains the result of the sql field with its operator and value during overloading
296             # expression, field, binded, types
297 19         72 my $opts = { @_ };
298             # So it can be called in chaining whether it contains data or not
299 19   33     138 $opts->{types} //= Module::Generic::Array->new;
300 19   33     499 return( bless( $opts => ref( $this ) || $this ) );
301             }
302            
303 0     0     sub binded { return( shift->{binded} ); }
304            
305 0     0     sub field { return( shift->{field} ); }
306              
307 0     0     sub types { return( shift->{types} ); }
308             }
309              
310             1;
311             # NOTE: POD
312             __END__
313              
314             =encoding utf8
315              
316             =head1 NAME
317              
318             DB::Object::Fields::Field - Table Field Object
319              
320             =head1 SYNOPSIS
321              
322             my $dbh = DB::Object->connect({
323             driver => 'Pg',
324             conf_file => $conf,
325             database => 'my_shop',
326             host => 'localhost',
327             login => 'super_admin',
328             schema => 'auth',
329             # debug => 3,
330             }) || bailout( "Unable to connect to sql server on host localhost: ", DB::Object->error );
331             my $tbl_object = $dbh->customers || die( "Unable to get the customers table object: ", $dbh->error, "\n" );
332             my $fields = $tbl_object->fields;
333             print( "Fields for table \"", $tbl_object->name, "\": ", Dumper( $fields ), "\n" );
334             my $c = $tbl_object->fo->currency;
335             print( "Got field object for currency: \"", ref( $c ), "\": '$c'\n" );
336             printf( "Name: %s\n", $c->name );
337             printf( "Type: %s\n", $c->type );
338             printf( "Default: %s\n", $c->default );
339             printf( "Position: %s\n", $c->pos );
340             printf( "Table: %s\n", $c->table );
341             printf( "Database: %s\n", $c->database );
342             printf( "Schema: %s\n", $c->schema );
343             printf( "Next field: %s (%s)\n", $c->next, ref( $c->next ) );
344             print( "Showing name fully qualified: ", $c->prefixed( 3 )->name, "\n" );
345             # would print: my_shop.public.customers.currency
346             print( "Trying again (should keep prefix): ", $c->name, "\n" );
347             # would print again: my_shop.public.customers.currency
348             print( "Now cancel prefixing at the table fields level.\n" );
349             $tbl_object->fo->prefixed( 0 );
350             print( "Showing name fully qualified again (should not be prefixed): ", $c->name, "\n" );
351             # would print currency
352             print( "First element is: ", $c->first, "\n" );
353             print( "Last element is: ", $c->last, "\n" );
354             # Works also with the operators +, -, *, /, %, <, <=, >, >=, !=, <<, >>, &, |, ^, ==
355             my $table = $dbh->dummy;
356             $table->select( $c + 10 ); # SELECT currency + 10 FROM dummy;
357             $c == 'NULL' # currency IS NULL
358              
359             You can also use a L<DB::Object::Statement> as a value in the operation:
360              
361             my $tbl = $dbh->services || die( "Unable to get the table object \"services\": ", $dbh->error );
362             my $userv_tbl = $dbh->user_services || die( "Unable to get the table object \"user_services\": ", $tbl->->error );
363             $tbl->where( $tbl->fo->name == '?' );
364             my $sub_sth = $tbl->select( 'id' ) || die( "Unable to prepare the sql query to get the service id: ", $tbl->error );
365             $userv_tbl->where(
366             $dbh->AND(
367             $tbl->fo->user_id == '?',
368             $tbl->fo->service_id == $sub_sth
369             )
370             );
371             my $query = $userv_tbl->delete->as_string || die( $tbl->error );
372              
373             This would yield:
374              
375             DELETE FROM user_services WHERE user_id = ? AND name = (SELECT id FROM services WHERE name = ?)
376              
377             =head1 VERSION
378              
379             v1.0.2
380              
381             =head1 DESCRIPTION
382              
383             This is a table field object as instantiated by L<DB::Object::Fields>
384              
385             =head1 CONSTRUCTOR
386              
387             =head2 new
388              
389             Takes an hash or hash reference of parameters and this will create a new L<DB::Object::Fields::Field> object.
390              
391             =over 4
392              
393             =item I<debug>
394              
395             Toggles debug mode on/off
396              
397             =item I<default>
398              
399             =item I<name>
400              
401             The table column name.
402              
403             An error will be returned if this value is not provided upon instantiation.
404              
405             =item I<pos>
406              
407             The table column position in the table.
408              
409             =item I<prefixed>
410              
411             Defaults to 0
412              
413             =item I<query_object>
414              
415             The L<DB::Object::Query> object.
416              
417             =item I<table_object>
418              
419             The L<DB::Object::Tables> object.
420              
421             An error will be returned if this value is not provided upon instantiation.
422              
423             =item I<type>
424              
425             The column data type.
426              
427             =back
428              
429             =head1 METHODS
430              
431             =head2 as_string
432              
433             This returns the name of the field, possibly prefixed
434              
435             This is also called to stringify the object
436              
437             print( "Field is: $field\n" );
438              
439             =head2 constant
440              
441             A data type constant set by L<DB::Object::Table/structure>. This helps determine how to deal with some fields.
442              
443             This is an hash object that contains 3 properties:
444              
445             =over 4
446              
447             =item I<constant>
448              
449             An integer set by the database driver to represent the constant
450              
451             =item I<name>
452              
453             The constant name, e.g. C<PG_JSONB>
454              
455             =item I<type>
456              
457             The data type, e.g. C<jsonb>
458              
459             =back
460              
461             =head2 database
462              
463             Returns the name of the database this field is attached to.
464              
465             =head2 database_object
466              
467             Returns the database object, ie the one used to make sql queries
468              
469             =head2 default
470              
471             Returns the default value, if any, for that field.
472              
473             =head2 first
474              
475             Returns the first field in the table.
476              
477             =head2 last
478              
479             Returns the last field in the table.
480              
481             =head2 name
482              
483             Returns the field name. This is also what is returned when object is stringified. For example
484              
485             my $c = $tbl_object->fo->last_name;
486             print( "$c\n" );
487             # will produce "last_name"
488              
489             The output is altered by the use of B<prefixed>. See below.
490              
491             =head2 next
492              
493             Returns the next field object.
494              
495             =head2 pos
496              
497             Returns the position of the field in the table. This is an integer starting from 1.
498              
499             =head2 prefixed
500              
501             Called without argument, this will instruct the field name to be returned prefixed by the table name.
502              
503             print( $tbl_object->fo->last_name->prefixed, "\n" );
504             # would produce my_shop.last_name
505              
506             B<prefixed> can also be called with an integer as argument. 1 will prefix it with the table name, 2 with the schema name and 3 with the database name.
507              
508             =head2 prev
509              
510             Returns the previous field object.
511              
512             =head2 query_object
513              
514             The query object (L<DB::Object::Query> or one of its descendant)
515              
516             =head2 schema
517              
518             Returns the table schema to which this field is attached.
519              
520             =head2 table
521              
522             Returns the table name for this field.
523              
524             =head2 table_name
525              
526             Same as above. This returns the table name.
527              
528             =head2 table_object
529              
530             Returns the table object which is a L<DB::Object::Tables> object.
531              
532             =head2 type
533              
534             Returns the field type such as C<jsonb>, Cjson>, C<varchar>, C<integer>, etc.
535              
536             See also L</constant> for an even more accurate data type, and the driver associated constant that is used for binding values to placeholders.
537              
538             =head2 _find_siblings
539              
540             Given a field position from 1 to n, this will find and return the field object. It returns undef or empty list if none could be found.
541              
542             =head1 OVERLOADING
543              
544             The following operators are overloaded:
545              
546             +, -, *, /, %, <, <=, >, >=, !=, <<, >>, lt, gt, le, ge, ne, &, |, ^, ==, eq, ~~
547              
548             Thus a field named "dummy" could be used like:
549              
550             $f + 10
551              
552             which would become:
553              
554             dummy + 10
555              
556             And this works too:
557              
558             10 + $f # 10 + dummy
559              
560             Another example, which works in PostgreSQL:
561              
562             $ip_tbl->where( 'inet 192.16.1.20' << $ip_tbl->fo->ip_addr );
563             my $ref = $ip_tbl->select->fetchrow_hashref;
564              
565             The equal operator C<==> would become C<IS>:
566              
567             $f == 'NULL' # dummy IS NULL
568              
569             Note that you have to take care of quotes yourself, because there is no way to tell if the right hand side is a string or a function
570              
571             $f == q{'JPY'} # dummy IS 'JPY'
572              
573             or, to insert a placeholder
574              
575             $f == '?' # dummy IS ?
576             my $sth = $table->select( $f == '?' ); # SELECT dummy IS ? FROM some_table
577             my $row = $sth->exec( 'JPY' )->fetchrow;
578              
579             of course
580              
581             my $sth = $table->select( dummy => '?' );
582              
583             also works
584              
585             The C<=~> and C<!~> operators cannot be overloaded in perl, so for regular expressions, use the C<REGEXP> function if available, or provided the expression directly as a string:
586              
587             $table->select( "currency ~ '^[A-Z]{3}$'" );
588              
589             If you want to use placeholder in the value provided, you will have to provide a C<?> in the value next to the operator. This module will not parse the value used with the operation, so if you wanted to use a placeholder in:
590              
591             $f == "'JPY'"
592              
593             Simply provide:
594              
595             $f == '?'
596              
597             You can use the search operator C<~~> for SQL Full Text Search and it would be converted into C<@@>:
598              
599             Let's imagine a table C<articles> in a L<PostgreSQL database|https://www.postgresql.org/docs/current/textsearch.html>, such as:
600              
601             CREATE TABLE articles (
602             id SERIAL PRIMARY KEY,
603             title TEXT NOT NULL,
604             content TEXT NOT NULL,
605             ts TSVECTOR GENERATED ALWAYS AS
606             (setweight(to_tsvector('english', coalesce(title, '')), 'A') ||
607             setweight(to_tsvector('english', coalesce(content, '')), 'B')) STORED
608             );
609              
610             them you coud do:
611              
612             $tbl->where(
613             \"websearch_to_tsquery(?)" ~~ $tbl->fo->ts,
614             );
615              
616             and this would create a C<WHERE> clause, such as:
617              
618             WHERE websearch_to_tsquery(?) @@ ts
619              
620             See L<PostgreSQL documentation|https://www.postgresql.org/docs/current/textsearch.html> for more details.
621              
622             but, under L<SQLite|https://www.sqlite.org/fts5.html>, this is not necessary, because the Full Text Search syntax is different:
623              
624             Create a FTS-enabled virtual table.
625              
626             CREATE VIRTUAL TABLE articles
627             USING FTS5(title, content);
628              
629             then query it:
630              
631             SELECT * FROM articles WHERE articles MATCH(?);
632              
633             See L<SQLite documentation|https://www.sqlite.org/fts5.html> for more details.
634              
635             and, in a L<MySQL database|https://dev.mysql.com/doc/refman/8.0/en/fulltext-search.html>, also unnecessary, because a bit different:
636              
637             CREATE TABLE articles (
638             id INT UNSIGNED AUTO_INCREMENT NOT NULL PRIMARY KEY,
639             title TEXT NOT NULL,
640             content TEXT NOT NULL,
641             FULLTEXT (title,content)
642             ) ENGINE=InnoDB;
643              
644             then:
645              
646             SELECT * FROM articles WHERE MATCH(title,content) AGAINST(? IN NATURAL LANGUAGE MODE);
647              
648             See L<MySQL documentation|https://dev.mysql.com/doc/refman/8.0/en/fulltext-search.html> for more details.
649              
650             =head1 SEE ALSO
651              
652             L<perl>
653              
654             =head1 AUTHOR
655              
656             Jacques Deguest E<lt>F<jack@deguest.jp>E<gt>
657              
658             =head1 COPYRIGHT & LICENSE
659              
660             Copyright (c) 2020-2021 DEGUEST Pte. Ltd.
661              
662             You can use, copy, modify and redistribute this package and associated
663             files under the same terms as Perl itself.
664              
665             =cut