File Coverage

lib/DB/Object/Fields/Field.pm
Criterion Covered Total %
statement 110 166 66.2
branch 25 58 43.1
condition 14 32 43.7
subroutine 43 66 65.1
pod 19 19 100.0
total 211 341 61.8


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   107487 use strict;
  2         10  
  2         61  
17 2     2   11 use warnings;
  2         3  
  2         48  
18 2     2   572 use common::sense;
  2         14  
  2         14  
19 2     2   564 use parent qw( Module::Generic );
  2         308  
  2         20  
20 2     2   12241017 use vars qw( $VERSION );
  2         5  
  2         83  
21 2     2   593 use Devel::Confess;
  2         7295  
  2         16  
22 2     2   712 use Module::Generic::Array;
  2         9402  
  2         1281  
23             use overload (
24             '""' => 'as_string',
25 2     2   28 'bool' => sub{1},
26 2     2   20 '+' => sub{ &_op_overload( @_, '+' ) },
27 1     1   29 '-' => sub{ &_op_overload( @_, '-' ) },
28 1     1   16 '*' => sub{ &_op_overload( @_, '*' ) },
29 1     1   9 '/' => sub{ &_op_overload( @_, '/' ) },
30 1     1   15 '%' => sub{ &_op_overload( @_, '%' ) },
31 1     1   15 '<' => sub{ &_op_overload( @_, '<' ) },
32 1     1   2401 '>' => sub{ &_op_overload( @_, '>' ) },
33 1     1   13 '<=' => sub{ &_op_overload( @_, '<=' ) },
34 1     1   10 '>=' => sub{ &_op_overload( @_, '>=' ) },
35 1     1   23 '!=' => sub{ &_op_overload( @_, '<>' ) },
36 3     3   14 '<<' => sub{ &_op_overload( @_, '<<' ) },
37 1     1   10 '>>' => 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   9 '&' => sub{ &_op_overload( @_, '&' ) },
44 1     1   14 '^' => sub{ &_op_overload( @_, '^' ) },
45 1     1   11 '|' => sub{ &_op_overload( @_, '|' ) },
46 1     1   5 '==' => sub{ &_op_overload( @_, '=' ) },
47 1     1   5 'eq' => sub{ &_op_overload( @_, 'IS' ) },
48             # Full Text Search operator
49 0     0   0 '~~' => sub{ &_op_overload( @_, '@@' ) },
50 2         84 fallback => 1,
51 2     2   14 );
  2         3  
52 2     2   647 use Want;
  2         5  
  2         119  
53 2     2   38 our( $VERSION ) = 'v1.0.2';
54             };
55              
56 2     2   9 use strict;
  2         4  
  2         41  
57 2     2   8 use warnings;
  2         5  
  2         702  
58              
59             sub init
60             {
61 1     1 1 194 my $self = shift( @_ );
62 1         124 $self->{default} = '';
63 1         25 $self->{name} = '';
64 1         22 $self->{pos} = '';
65 1         12 $self->{prefixed} = 0;
66 1         14 $self->{query_object} = '';
67 1         11 $self->{table_object} = '';
68 1         15 $self->{type} = '';
69 1         25 $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       26 $self->SUPER::init( @_ ) || return( $self->pass_error );
72 1 50       19 return( $self->error( "No table object was provided." ) ) if( !$self->{table_object} );
73 1 50       17 return( $self->error( "Table object provided is not an object." ) ) if( !$self->_is_object( $self->{table_object} ) );
74 1 50       30 return( $self->error( "Table object provided is not a DB::Object::Tables object." ) ) if( !$self->{table_object}->isa( 'DB::Object::Tables' ) );
75 1 50       25 return( $self->error( "No name was provided for this field." ) ) if( !$self->{name} );
76 1         20 $self->{trace} = $self->_get_stack_trace;
77 1         303 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 448 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 73     73 1 597 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 21     21 1 95 my $self = shift( @_ );
104 2     2   14 no overloading;
  2         3  
  2         1871  
105 21 100       60 if( @_ )
106             {
107 1         10 $self->{name} = shift( @_ );
108             }
109 21         48 my $name = $self->{name};
110 21         70 my $trace = $self->_get_stack_trace;
111 21         6198 my $alias = $self->query_object->table_alias;
112 21 50       18596 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 21         223 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 22     22 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 74     74 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 20     20   135 my( $self, $val, $swap, $op ) = @_;
207 20 50       104 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 20         402 my $field = $self->name;
214 20         127 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 20 50       74 $op = $map->{ $op } if( exists( $map->{ $op } ) );
230             # $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 20 100 0     101 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       193 $val = $self->database_object->quote( $val, $self->constant->constant ) if( $self->database_object );
244             }
245            
246 20         168 my $types;
247             # If the value is a statement object, stringify it, surround it with parenthesis and use it
248 20 100       83 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         14 $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         23 $val = $$val;
264             }
265 20 100 66     315 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         6  
  2         40  
285 2     2   9 use common::sense;
  2         3  
  2         9  
286             use overload (
287 20     20   7176 '""' => sub{ return( $_[0]->{expression} ) },
288 2         17 fallback => 1,
289 2     2   180 );
  2         3  
290             our $VERSION = 'v0.1.0';
291              
292             sub new
293             {
294 20     20   40 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 20         77 my $opts = { @_ };
298             # So it can be called in chaining whether it contains data or not
299 20   33     155 $opts->{types} //= Module::Generic::Array->new;
300 20   33     503 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<=>:
566              
567             $f == 'NULL' # dummy = NULL
568              
569             but, if you use perl's C<eq> instead of C<==>, you would get:
570              
571             $f eq 'NULL' # dummy IS NULL
572              
573             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
574              
575             $f == q{'JPY'} # dummy IS 'JPY'
576              
577             or, to insert a placeholder
578              
579             $f == '?' # dummy = ?
580             # or;
581             $f eq '?' # dummy IS ?
582             my $sth = $table->select( $f eq '?' ); # SELECT dummy IS ? FROM some_table
583             my $row = $sth->exec( 'JPY' )->fetchrow;
584              
585             of course
586              
587             my $sth = $table->select( dummy => '?' );
588              
589             also works
590              
591             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:
592              
593             $table->select( "currency ~ '^[A-Z]{3}$'" );
594              
595             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:
596              
597             $f == "'JPY'"
598              
599             Simply provide:
600              
601             $f == '?'
602              
603             You can use the search operator C<~~> for SQL Full Text Search and it would be converted into C<@@>:
604              
605             Let's imagine a table C<articles> in a L<PostgreSQL database|https://www.postgresql.org/docs/current/textsearch.html>, such as:
606              
607             CREATE TABLE articles (
608             id SERIAL PRIMARY KEY,
609             title TEXT NOT NULL,
610             content TEXT NOT NULL,
611             ts TSVECTOR GENERATED ALWAYS AS
612             (setweight(to_tsvector('english', coalesce(title, '')), 'A') ||
613             setweight(to_tsvector('english', coalesce(content, '')), 'B')) STORED
614             );
615              
616             them you coud do:
617              
618             $tbl->where(
619             \"websearch_to_tsquery(?)" ~~ $tbl->fo->ts,
620             );
621              
622             and this would create a C<WHERE> clause, such as:
623              
624             WHERE websearch_to_tsquery(?) @@ ts
625              
626             See L<PostgreSQL documentation|https://www.postgresql.org/docs/current/textsearch.html> for more details.
627              
628             but, under L<SQLite|https://www.sqlite.org/fts5.html>, this is not necessary, because the Full Text Search syntax is different:
629              
630             Create a FTS-enabled virtual table.
631              
632             CREATE VIRTUAL TABLE articles
633             USING FTS5(title, content);
634              
635             then query it:
636              
637             SELECT * FROM articles WHERE articles MATCH(?);
638              
639             See L<SQLite documentation|https://www.sqlite.org/fts5.html> for more details.
640              
641             and, in a L<MySQL database|https://dev.mysql.com/doc/refman/8.0/en/fulltext-search.html>, also unnecessary, because a bit different:
642              
643             CREATE TABLE articles (
644             id INT UNSIGNED AUTO_INCREMENT NOT NULL PRIMARY KEY,
645             title TEXT NOT NULL,
646             content TEXT NOT NULL,
647             FULLTEXT (title,content)
648             ) ENGINE=InnoDB;
649              
650             then:
651              
652             SELECT * FROM articles WHERE MATCH(title,content) AGAINST(? IN NATURAL LANGUAGE MODE);
653              
654             See L<MySQL documentation|https://dev.mysql.com/doc/refman/8.0/en/fulltext-search.html> for more details.
655              
656             =head1 SEE ALSO
657              
658             L<perl>
659              
660             =head1 AUTHOR
661              
662             Jacques Deguest E<lt>F<jack@deguest.jp>E<gt>
663              
664             =head1 COPYRIGHT & LICENSE
665              
666             Copyright (c) 2020-2021 DEGUEST Pte. Ltd.
667              
668             You can use, copy, modify and redistribute this package and associated
669             files under the same terms as Perl itself.
670              
671             =cut