1/*****************************************************************************
2
3Copyright (c) 1996, 2016, Oracle and/or its affiliates. All Rights Reserved.
4Copyright (c) 2017, 2018, MariaDB Corporation.
5
6This program is free software; you can redistribute it and/or modify it under
7the terms of the GNU General Public License as published by the Free Software
8Foundation; version 2 of the License.
9
10This program is distributed in the hope that it will be useful, but WITHOUT
11ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
12FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
13
14You should have received a copy of the GNU General Public License along with
15this program; if not, write to the Free Software Foundation, Inc.,
1651 Franklin Street, Suite 500, Boston, MA 02110-1335 USA
17
18*****************************************************************************/
19
20/**************************************************//**
21@file include/row0upd.h
22Update of a row
23
24Created 12/27/1996 Heikki Tuuri
25*******************************************************/
26
27#ifndef row0upd_h
28#define row0upd_h
29
30#include "data0data.h"
31#include "row0types.h"
32#include "btr0types.h"
33#include "dict0types.h"
34#include "trx0types.h"
35#include "btr0pcur.h"
36#include "que0types.h"
37#include "pars0types.h"
38
39/*********************************************************************//**
40Creates an update vector object.
41@return own: update vector object */
42UNIV_INLINE
43upd_t*
44upd_create(
45/*=======*/
46 ulint n, /*!< in: number of fields */
47 mem_heap_t* heap); /*!< in: heap from which memory allocated */
48/*********************************************************************//**
49Returns the number of fields in the update vector == number of columns
50to be updated by an update vector.
51@return number of fields */
52UNIV_INLINE
53ulint
54upd_get_n_fields(
55/*=============*/
56 const upd_t* update); /*!< in: update vector */
57#ifdef UNIV_DEBUG
58/*********************************************************************//**
59Returns the nth field of an update vector.
60@return update vector field */
61UNIV_INLINE
62upd_field_t*
63upd_get_nth_field(
64/*==============*/
65 const upd_t* update, /*!< in: update vector */
66 ulint n); /*!< in: field position in update vector */
67#else
68# define upd_get_nth_field(update, n) ((update)->fields + (n))
69#endif
70
71/*********************************************************************//**
72Sets an index field number to be updated by an update vector field. */
73UNIV_INLINE
74void
75upd_field_set_field_no(
76/*===================*/
77 upd_field_t* upd_field, /*!< in: update vector field */
78 ulint field_no, /*!< in: field number in a clustered
79 index */
80 dict_index_t* index);
81
82/** set field number to a update vector field, marks this field is updated
83@param[in,out] upd_field update vector field
84@param[in] field_no virtual column sequence num
85@param[in] index index */
86UNIV_INLINE
87void
88upd_field_set_v_field_no(
89 upd_field_t* upd_field,
90 ulint field_no,
91 dict_index_t* index);
92/*********************************************************************//**
93Returns a field of an update vector by field_no.
94@return update vector field, or NULL */
95UNIV_INLINE
96const upd_field_t*
97upd_get_field_by_field_no(
98/*======================*/
99 const upd_t* update, /*!< in: update vector */
100 ulint no, /*!< in: field_no */
101 bool is_virtual) /*!< in: if it is a virtual column */
102 MY_ATTRIBUTE((warn_unused_result));
103/*********************************************************************//**
104Writes into the redo log the values of trx id and roll ptr and enough info
105to determine their positions within a clustered index record.
106@return new pointer to mlog */
107byte*
108row_upd_write_sys_vals_to_log(
109/*==========================*/
110 dict_index_t* index, /*!< in: clustered index */
111 trx_id_t trx_id, /*!< in: transaction id */
112 roll_ptr_t roll_ptr,/*!< in: roll ptr of the undo log record */
113 byte* log_ptr,/*!< pointer to a buffer of size > 20 opened
114 in mlog */
115 mtr_t* mtr); /*!< in: mtr */
116/*********************************************************************//**
117Updates the trx id and roll ptr field in a clustered index record when
118a row is updated or marked deleted. */
119UNIV_INLINE
120void
121row_upd_rec_sys_fields(
122/*===================*/
123 rec_t* rec, /*!< in/out: record */
124 page_zip_des_t* page_zip,/*!< in/out: compressed page whose
125 uncompressed part will be updated, or NULL */
126 dict_index_t* index, /*!< in: clustered index */
127 const ulint* offsets,/*!< in: rec_get_offsets(rec, index) */
128 const trx_t* trx, /*!< in: transaction */
129 roll_ptr_t roll_ptr);/*!< in: DB_ROLL_PTR to the undo log */
130/*********************************************************************//**
131Sets the trx id or roll ptr field of a clustered index entry. */
132void
133row_upd_index_entry_sys_field(
134/*==========================*/
135 dtuple_t* entry, /*!< in/out: index entry, where the memory
136 buffers for sys fields are already allocated:
137 the function just copies the new values to
138 them */
139 dict_index_t* index, /*!< in: clustered index */
140 ulint type, /*!< in: DATA_TRX_ID or DATA_ROLL_PTR */
141 ib_uint64_t val); /*!< in: value to write */
142/*********************************************************************//**
143Creates an update node for a query graph.
144@return own: update node */
145upd_node_t*
146upd_node_create(
147/*============*/
148 mem_heap_t* heap); /*!< in: mem heap where created */
149/***********************************************************//**
150Writes to the redo log the new values of the fields occurring in the index. */
151void
152row_upd_index_write_log(
153/*====================*/
154 const upd_t* update, /*!< in: update vector */
155 byte* log_ptr,/*!< in: pointer to mlog buffer: must
156 contain at least MLOG_BUF_MARGIN bytes
157 of free space; the buffer is closed
158 within this function */
159 mtr_t* mtr); /*!< in: mtr into whose log to write */
160/***********************************************************//**
161Returns TRUE if row update changes size of some field in index or if some
162field to be updated is stored externally in rec or update.
163@return TRUE if the update changes the size of some field in index or
164the field is external in rec or update */
165ibool
166row_upd_changes_field_size_or_external(
167/*===================================*/
168 dict_index_t* index, /*!< in: index */
169 const ulint* offsets,/*!< in: rec_get_offsets(rec, index) */
170 const upd_t* update);/*!< in: update vector */
171/***********************************************************//**
172Returns true if row update contains disowned external fields.
173@return true if the update contains disowned external fields. */
174bool
175row_upd_changes_disowned_external(
176/*==============================*/
177 const upd_t* update) /*!< in: update vector */
178 MY_ATTRIBUTE((nonnull, warn_unused_result));
179/***********************************************************//**
180Replaces the new column values stored in the update vector to the
181record given. No field size changes are allowed. This function is
182usually invoked on a clustered index. The only use case for a
183secondary index is row_ins_sec_index_entry_by_modify() or its
184counterpart in ibuf_insert_to_index_page(). */
185void
186row_upd_rec_in_place(
187/*=================*/
188 rec_t* rec, /*!< in/out: record where replaced */
189 dict_index_t* index, /*!< in: the index the record belongs to */
190 const ulint* offsets,/*!< in: array returned by rec_get_offsets() */
191 const upd_t* update, /*!< in: update vector */
192 page_zip_des_t* page_zip);/*!< in: compressed page with enough space
193 available, or NULL */
194
195/***************************************************************//**
196Builds an update vector from those fields which in a secondary index entry
197differ from a record that has the equal ordering fields. NOTE: we compare
198the fields as binary strings!
199@return own: update vector of differing fields */
200upd_t*
201row_upd_build_sec_rec_difference_binary(
202/*====================================*/
203 const rec_t* rec, /*!< in: secondary index record */
204 dict_index_t* index, /*!< in: index */
205 const ulint* offsets,/*!< in: rec_get_offsets(rec, index) */
206 const dtuple_t* entry, /*!< in: entry to insert */
207 mem_heap_t* heap) /*!< in: memory heap from which allocated */
208 MY_ATTRIBUTE((warn_unused_result, nonnull));
209/** Builds an update vector from those fields, excluding the roll ptr and
210trx id fields, which in an index entry differ from a record that has
211the equal ordering fields. NOTE: we compare the fields as binary strings!
212@param[in] index clustered index
213@param[in] entry clustered index entry to insert
214@param[in] rec clustered index record
215@param[in] offsets rec_get_offsets(rec,index), or NULL
216@param[in] no_sys skip the system columns
217 DB_TRX_ID and DB_ROLL_PTR
218@param[in] trx transaction (for diagnostics),
219 or NULL
220@param[in] heap memory heap from which allocated
221@param[in,out] mysql_table NULL, or mysql table object when
222 user thread invokes dml
223@return own: update vector of differing fields, excluding roll ptr and
224trx id */
225upd_t*
226row_upd_build_difference_binary(
227 dict_index_t* index,
228 const dtuple_t* entry,
229 const rec_t* rec,
230 const ulint* offsets,
231 bool no_sys,
232 trx_t* trx,
233 mem_heap_t* heap,
234 TABLE* mysql_table)
235 MY_ATTRIBUTE((nonnull(1,2,3,7), warn_unused_result));
236/** Apply an update vector to an index entry.
237@param[in,out] entry index entry to be updated; the clustered index record
238 must be covered by a lock or a page latch to prevent
239 deletion (rollback or purge)
240@param[in] index index of the entry
241@param[in] update update vector built for the entry
242@param[in,out] heap memory heap for copying off-page columns */
243void
244row_upd_index_replace_new_col_vals_index_pos(
245 dtuple_t* entry,
246 const dict_index_t* index,
247 const upd_t* update,
248 mem_heap_t* heap)
249 MY_ATTRIBUTE((nonnull));
250/***********************************************************//**
251Replaces the new column values stored in the update vector to the index entry
252given. */
253void
254row_upd_index_replace_new_col_vals(
255/*===============================*/
256 dtuple_t* entry, /*!< in/out: index entry where replaced;
257 the clustered index record must be
258 covered by a lock or a page latch to
259 prevent deletion (rollback or purge) */
260 dict_index_t* index, /*!< in: index; NOTE that this may also be a
261 non-clustered index */
262 const upd_t* update, /*!< in: an update vector built for the
263 CLUSTERED index so that the field number in
264 an upd_field is the clustered index position */
265 mem_heap_t* heap) /*!< in: memory heap for allocating and
266 copying the new values */
267 MY_ATTRIBUTE((nonnull));
268/***********************************************************//**
269Replaces the new column values stored in the update vector. */
270void
271row_upd_replace(
272/*============*/
273 dtuple_t* row, /*!< in/out: row where replaced,
274 indexed by col_no;
275 the clustered index record must be
276 covered by a lock or a page latch to
277 prevent deletion (rollback or purge) */
278 row_ext_t** ext, /*!< out, own: NULL, or externally
279 stored column prefixes */
280 const dict_index_t* index, /*!< in: clustered index */
281 const upd_t* update, /*!< in: an update vector built for the
282 clustered index */
283 mem_heap_t* heap); /*!< in: memory heap */
284/** Replaces the virtual column values stored in a dtuple with that of
285a update vector.
286@param[in,out] row dtuple whose column to be updated
287@param[in] table table
288@param[in] update an update vector built for the clustered index
289@param[in] upd_new update to new or old value
290@param[in,out] undo_row undo row (if needs to be updated)
291@param[in] ptr remaining part in update undo log */
292void
293row_upd_replace_vcol(
294 dtuple_t* row,
295 const dict_table_t* table,
296 const upd_t* update,
297 bool upd_new,
298 dtuple_t* undo_row,
299 const byte* ptr);
300
301/***********************************************************//**
302Checks if an update vector changes an ordering field of an index record.
303
304This function is fast if the update vector is short or the number of ordering
305fields in the index is small. Otherwise, this can be quadratic.
306NOTE: we compare the fields as binary strings!
307@return TRUE if update vector changes an ordering field in the index record */
308ibool
309row_upd_changes_ord_field_binary_func(
310/*==================================*/
311 dict_index_t* index, /*!< in: index of the record */
312 const upd_t* update, /*!< in: update vector for the row; NOTE: the
313 field numbers in this MUST be clustered index
314 positions! */
315#ifdef UNIV_DEBUG
316 const que_thr_t*thr, /*!< in: query thread */
317#endif /* UNIV_DEBUG */
318 const dtuple_t* row, /*!< in: old value of row, or NULL if the
319 row and the data values in update are not
320 known when this function is called, e.g., at
321 compile time */
322 const row_ext_t*ext, /*!< NULL, or prefixes of the externally
323 stored columns in the old row */
324 ulint flag) /*!< in: ROW_BUILD_NORMAL,
325 ROW_BUILD_FOR_PURGE or ROW_BUILD_FOR_UNDO */
326 MY_ATTRIBUTE((nonnull(1,2), warn_unused_result));
327#ifdef UNIV_DEBUG
328# define row_upd_changes_ord_field_binary(index,update,thr,row,ext) \
329 row_upd_changes_ord_field_binary_func(index,update,thr,row,ext,0)
330#else /* UNIV_DEBUG */
331# define row_upd_changes_ord_field_binary(index,update,thr,row,ext) \
332 row_upd_changes_ord_field_binary_func(index,update,row,ext,0)
333#endif /* UNIV_DEBUG */
334/***********************************************************//**
335Checks if an FTS indexed column is affected by an UPDATE.
336@return offset within fts_t::indexes if FTS indexed column updated else
337ULINT_UNDEFINED */
338ulint
339row_upd_changes_fts_column(
340/*=======================*/
341 dict_table_t* table, /*!< in: table */
342 upd_field_t* upd_field); /*!< in: field to check */
343/***********************************************************//**
344Checks if an FTS Doc ID column is affected by an UPDATE.
345@return whether Doc ID column is affected */
346bool
347row_upd_changes_doc_id(
348/*===================*/
349 dict_table_t* table, /*!< in: table */
350 upd_field_t* upd_field) /*!< in: field to check */
351 MY_ATTRIBUTE((nonnull, warn_unused_result));
352/***********************************************************//**
353Checks if an update vector changes an ordering field of an index record.
354This function is fast if the update vector is short or the number of ordering
355fields in the index is small. Otherwise, this can be quadratic.
356NOTE: we compare the fields as binary strings!
357@return TRUE if update vector may change an ordering field in an index
358record */
359ibool
360row_upd_changes_some_index_ord_field_binary(
361/*========================================*/
362 const dict_table_t* table, /*!< in: table */
363 const upd_t* update);/*!< in: update vector for the row */
364/** Stores to the heap the row on which the node->pcur is positioned.
365@param[in] node row update node
366@param[in] thd mysql thread handle
367@param[in,out] mysql_table NULL, or mysql table object when
368 user thread invokes dml */
369void
370row_upd_store_row(
371 upd_node_t* node,
372 THD* thd,
373 TABLE* mysql_table);
374/***********************************************************//**
375Updates a row in a table. This is a high-level function used
376in SQL execution graphs.
377@return query thread to run next or NULL */
378que_thr_t*
379row_upd_step(
380/*=========*/
381 que_thr_t* thr); /*!< in: query thread */
382/*********************************************************************//**
383Parses the log data of system field values.
384@return log data end or NULL */
385byte*
386row_upd_parse_sys_vals(
387/*===================*/
388 const byte* ptr, /*!< in: buffer */
389 const byte* end_ptr,/*!< in: buffer end */
390 ulint* pos, /*!< out: TRX_ID position in record */
391 trx_id_t* trx_id, /*!< out: trx id */
392 roll_ptr_t* roll_ptr);/*!< out: roll ptr */
393/*********************************************************************//**
394Updates the trx id and roll ptr field in a clustered index record in database
395recovery. */
396void
397row_upd_rec_sys_fields_in_recovery(
398/*===============================*/
399 rec_t* rec, /*!< in/out: record */
400 page_zip_des_t* page_zip,/*!< in/out: compressed page, or NULL */
401 const ulint* offsets,/*!< in: array returned by rec_get_offsets() */
402 ulint pos, /*!< in: TRX_ID position in rec */
403 trx_id_t trx_id, /*!< in: transaction id */
404 roll_ptr_t roll_ptr);/*!< in: roll ptr of the undo log record */
405/*********************************************************************//**
406Parses the log data written by row_upd_index_write_log.
407@return log data end or NULL */
408byte*
409row_upd_index_parse(
410/*================*/
411 const byte* ptr, /*!< in: buffer */
412 const byte* end_ptr,/*!< in: buffer end */
413 mem_heap_t* heap, /*!< in: memory heap where update vector is
414 built */
415 upd_t** update_out);/*!< out: update vector */
416
417
418/* Update vector field */
419struct upd_field_t{
420 unsigned field_no:16; /*!< field number in an index, usually
421 the clustered index, but in updating
422 a secondary index record in btr0cur.cc
423 this is the position in the secondary
424 index. If this field is a virtual
425 column, then field_no represents
426 the nth virtual column in the table */
427 unsigned orig_len:16; /*!< original length of the locally
428 stored part of an externally stored
429 column, or 0 */
430 que_node_t* exp; /*!< expression for calculating a new
431 value: it refers to column values and
432 constants in the symbol table of the
433 query graph */
434 dfield_t new_val; /*!< new value for the column */
435 dfield_t* old_v_val; /*!< old value for the virtual column */
436};
437
438
439/* check whether an update field is on virtual column */
440#define upd_fld_is_virtual_col(upd_fld) \
441 (((upd_fld)->new_val.type.prtype & DATA_VIRTUAL) == DATA_VIRTUAL)
442
443/* set DATA_VIRTUAL bit on update field to show it is a virtual column */
444#define upd_fld_set_virtual_col(upd_fld) \
445 ((upd_fld)->new_val.type.prtype |= DATA_VIRTUAL)
446
447/* Update vector structure */
448struct upd_t{
449 mem_heap_t* heap; /*!< heap from which memory allocated */
450 ulint info_bits; /*!< new value of info bits to record;
451 default is 0 */
452 dtuple_t* old_vrow; /*!< pointer to old row, used for
453 virtual column update now */
454 ulint n_fields; /*!< number of update fields */
455 upd_field_t* fields; /*!< array of update fields */
456 byte vers_sys_value[8]; /*!< buffer for updating system fields */
457
458 /** Append an update field to the end of array
459 @param[in] field an update field */
460 void append(const upd_field_t& field)
461 {
462 fields[n_fields++] = field;
463 }
464
465 /** Determine if the given field_no is modified.
466 @return true if modified, false otherwise. */
467 bool is_modified(const ulint field_no) const
468 {
469 for (ulint i = 0; i < n_fields; ++i) {
470 if (field_no == fields[i].field_no) {
471 return(true);
472 }
473 }
474 return(false);
475 }
476
477 /** Determine if the update affects a system versioned column or row_end. */
478 bool affects_versioned() const
479 {
480 for (ulint i = 0; i < n_fields; i++) {
481 dtype_t type = fields[i].new_val.type;
482 if (type.is_versioned()) {
483 return true;
484 }
485 // versioned DELETE is UPDATE SET row_end=NOW
486 if (type.vers_sys_end()) {
487 return true;
488 }
489 }
490 return false;
491 }
492
493#ifdef UNIV_DEBUG
494 bool validate() const
495 {
496 for (ulint i = 0; i < n_fields; ++i) {
497 dfield_t* field = &fields[i].new_val;
498 if (dfield_is_ext(field)) {
499 ut_ad(dfield_get_len(field)
500 >= BTR_EXTERN_FIELD_REF_SIZE);
501 }
502 }
503 return(true);
504 }
505#endif // UNIV_DEBUG
506
507};
508
509/** Kinds of update operation */
510enum delete_mode_t {
511 NO_DELETE = 0, /*!< this operation does not delete */
512 PLAIN_DELETE, /*!< ordinary delete */
513 VERSIONED_DELETE /*!< update old and insert a new row */
514};
515
516/* Update node structure which also implements the delete operation
517of a row */
518
519struct upd_node_t{
520 que_common_t common; /*!< node type: QUE_NODE_UPDATE */
521 delete_mode_t is_delete; /*!< kind of DELETE */
522 ibool searched_update;
523 /* TRUE if searched update, FALSE if
524 positioned */
525 bool in_mysql_interface;
526 /* whether the update node was created
527 for the MySQL interface */
528 dict_foreign_t* foreign;/* NULL or pointer to a foreign key
529 constraint if this update node is used in
530 doing an ON DELETE or ON UPDATE operation */
531 upd_node_t* cascade_node;/* NULL or an update node template which
532 is used to implement ON DELETE/UPDATE CASCADE
533 or ... SET NULL for foreign keys */
534 mem_heap_t* cascade_heap;
535 /*!< NULL or a mem heap where cascade
536 node is created.*/
537 sel_node_t* select; /*!< query graph subtree implementing a base
538 table cursor: the rows returned will be
539 updated */
540 btr_pcur_t* pcur; /*!< persistent cursor placed on the clustered
541 index record which should be updated or
542 deleted; the cursor is stored in the graph
543 of 'select' field above, except in the case
544 of the MySQL interface */
545 dict_table_t* table; /*!< table where updated */
546 upd_t* update; /*!< update vector for the row */
547 ulint update_n_fields;
548 /* when this struct is used to implement
549 a cascade operation for foreign keys, we store
550 here the size of the buffer allocated for use
551 as the update vector */
552 sym_node_list_t columns;/* symbol table nodes for the columns
553 to retrieve from the table */
554 ibool has_clust_rec_x_lock;
555 /* TRUE if the select which retrieves the
556 records to update already sets an x-lock on
557 the clustered record; note that it must always
558 set at least an s-lock */
559 ulint cmpl_info;/* information extracted during query
560 compilation; speeds up execution:
561 UPD_NODE_NO_ORD_CHANGE and
562 UPD_NODE_NO_SIZE_CHANGE, ORed */
563 /*----------------------*/
564 /* Local storage for this graph node */
565 ulint state; /*!< node execution state */
566 dict_index_t* index; /*!< NULL, or the next index whose record should
567 be updated */
568 dtuple_t* row; /*!< NULL, or a copy (also fields copied to
569 heap) of the row to update; this must be reset
570 to NULL after a successful update */
571 dtuple_t* historical_row; /*!< historical row used in
572 CASCADE UPDATE/SET NULL;
573 allocated from historical_heap */
574 mem_heap_t* historical_heap; /*!< heap for historical row insertion;
575 created when row to update is located;
576 freed right before row update */
577 row_ext_t* ext; /*!< NULL, or prefixes of the externally
578 stored columns in the old row */
579 dtuple_t* upd_row;/* NULL, or a copy of the updated row */
580 row_ext_t* upd_ext;/* NULL, or prefixes of the externally
581 stored columns in upd_row */
582 mem_heap_t* heap; /*!< memory heap used as auxiliary storage;
583 this must be emptied after a successful
584 update */
585 /*----------------------*/
586 sym_node_t* table_sym;/* table node in symbol table */
587 que_node_t* col_assign_list;
588 /* column assignment list */
589 ulint magic_n;
590
591 /** Also set row_start = CURRENT_TIMESTAMP/trx->id
592 @param[in] trx transaction */
593 void make_versioned_update(const trx_t* trx);
594 /** Only set row_end = CURRENT_TIMESTAMP/trx->id.
595 Do not touch other fields at all.
596 @param[in] trx transaction */
597 void make_versioned_delete(const trx_t* trx);
598
599private:
600 /** Appends row_start or row_end field to update vector and sets a
601 CURRENT_TIMESTAMP/trx->id value to it.
602 Supposed to be called only by make_versioned_update() and
603 make_versioned_delete().
604 @param[in] trx transaction
605 @param[in] vers_sys_idx table->row_start or table->row_end */
606 void make_versioned_helper(const trx_t* trx, ulint idx);
607};
608
609#define UPD_NODE_MAGIC_N 1579975
610
611/* Node execution states */
612#define UPD_NODE_SET_IX_LOCK 1 /* execution came to the node from
613 a node above and if the field
614 has_clust_rec_x_lock is FALSE, we
615 should set an intention x-lock on
616 the table */
617#define UPD_NODE_UPDATE_CLUSTERED 2 /* clustered index record should be
618 updated */
619#define UPD_NODE_INSERT_CLUSTERED 3 /* clustered index record should be
620 inserted, old record is already delete
621 marked */
622#define UPD_NODE_UPDATE_ALL_SEC 5 /* an ordering field of the clustered
623 index record was changed, or this is
624 a delete operation: should update
625 all the secondary index records */
626#define UPD_NODE_UPDATE_SOME_SEC 6 /* secondary index entries should be
627 looked at and updated if an ordering
628 field changed */
629
630/* Compilation info flags: these must fit within 3 bits; see trx0rec.h */
631#define UPD_NODE_NO_ORD_CHANGE 1 /* no secondary index record will be
632 changed in the update and no ordering
633 field of the clustered index */
634#define UPD_NODE_NO_SIZE_CHANGE 2 /* no record field size will be
635 changed in the update */
636
637
638#include "row0upd.ic"
639
640#endif
641