# Bbuuzzb low level system catalog management module.
# Rick Smereka, Copyright (C) 2005-2006.

# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.

# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.

# You should have received a copy of the GNU General Public License
# along with this program; if not, get a copy via the Internet at
# http://gnu.org/copyleft/gpl.html or write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston,
# MA 02111-1307 USA

# You can contact the author via email at rsmereka@future-lab.com

# This tcl version stores catalog details for a single table in
# the 'dbengcat_details' array.

# Initial tcl version. Aug/2005, Rick Smereka

# Renamed procedure 'dbeng_catalog_list' to 'db_get_catalog_list'.
# Apr/2006, Rick Smereka

# global data
# catalog field definitions
const DBENG_CATALOG_FIELD_TNAME 1
const DBENG_CATALOG_FIELD_FNAME 2
const DBENG_CATALOG_FIELD_REPLICATE 3
const DBENG_CATALOG_FIELD_AUTOPACK 4
const DBENG_CATALOG_SSF_PORT 1
const DBENG_CATALOG_SSF_TNAME 2
const DBENG_CATALOG_TYPE_TNAME 1
const DBENG_CATALOG_TYPE_FNAME 2
const DBENG_CATALOG_FNAME_IND "%"

# catalog details array
set dbengcat_details(physical_name) ""
set dbengcat_details(apack) 0

# catalog tid
set cat 0

proc dbeng_catalog_new {tname fname} {
   # Create a new entry in the system catalog. The table name (logical)
   # is expected in 'tname' and the physical file name is expected
   # in 'fname'. The table logical name must be unique.
   # Returns a 'dbeng' code.
   global cat DBENG_OK DBENG_INVALID_FUNCTION DBENG_CATALOG_ENTRY_EXISTS
   global DBENG_CATALOG_FIELD_TNAME DBENG_CATALOG_FIELD_FNAME
   set mname "dbeng_catalog_new"

   logman "$mname:enter"

   if {$tname == ""} {
      db_io_code_log $mname "exit:empty(tname)" $DBENG_INVALID_FUNCTION
      return $DBENG_INVALID_FUNCTION
   }

   if {$fname == ""} {
      db_io_code_log $mname "exit:empty(fname)" $DBENG_INVALID_FUNCTION
      return $DBENG_INVALID_FUNCTION
   }

   # open catalog
   set ret [dbeng_catalog_open]

   if {$ret != $DBENG_OK} {
      db_io_code_log $mname "exit:bad rc from dbeng_catalog_open" $ret
      return $ret
   }

   if {[dbeng_catalog_tfind $tname] == $DBENG_OK} {
      db_io_code_log $mname "exit:catalog rec already exists" \
                     $DBENG_CATALOG_ENTRY_EXISTS
      dbeng_catalog_close
      return $DBENG_CATALOG_ENTRY_EXISTS
   }

   # add a new catalog entry
   set ret [db_new $cat]

   if {$ret != $DBENG_OK} {
      db_io_code_log $mname "exit:bad ret from db_new" $ret
      dbeng_catalog_close
      return $ret
   }

   set ret [db_put_field $cat $DBENG_CATALOG_FIELD_TNAME $tname]

   if {$ret != $DBENG_OK} {
      db_io_code_log $mname "exit:bad ret from db_put_field(tname)" $ret
      dbeng_catalog_close
      return $ret
   }

   set ret [db_put_field $cat $DBENG_CATALOG_FIELD_FNAME $fname]

   if {$ret != $DBENG_OK} {
      db_io_code_log $mname "exit:bad ret from db_put_field(fname)" $ret
      dbeng_catalog_close
      return $ret
   }
      
   set ret [db_write $cat]

   if {$ret != $DBENG_OK} {
      db_io_code_log $mname "exit:bad ret from db_write" $ret
      dbeng_catalog_close
      return $ret
   }

   set ret [dbeng_catalog_close]

   if {$ret != $DBENG_OK} {
      db_io_code_log $mname "exit:bad ret from dbeng_catalog_close" $ret
      return $ret
   }

   db_io_code_log $mname "normal exit" $DBENG_OK
   return $DBENG_OK
}

proc dbeng_catalog_new_table {iname oname systable_flag} {
   # Translate new table name(s) to the proper output
   # name. 'iname' may contain a single name or two
   # names comma delimited. A single name is assumed
   # to be the table physical file name. Two names are
   # assumed to be the table logical name followed by
   # the physical name. Upon success, the translated
   # name is placed into 'oname'. Returns a 'dbeng'
   # code.
   global DBENG_OK DBENG_INVALID_FUNCTION DBENG_CANNOT_CREATE_TABLE
   global DBENG_INTERNAL_ERROR DBENG_CATALOG_FNAME_IND
   upvar $oname uoname
   set mname "dbeng_catalog_new_table"

   logman "$mname:enter"
   set uoname ""
   set tmp_oname ""

   if {$iname == ""} {
      db_io_code_log $mname "exit:empty(iname)" $DBENG_INVALID_FUNCTION
      return $DBENG_INVALID_FUNCTION
   }

   db_config_get_catalog_flag catalog_flag
   set nwords [ll_words $iname ","]

   if {$nwords == 1} {
      # only physical file name given
      if {$systable_flag} {
         # if its a system table, use the same name
         set tmp_oname $iname
      }

      if {!$systable_flag} {
         if {$catalog_flag} {
            # if catalog is on, prefix the name with the file indicator
            set tmp_oname "$DBENG_CATALOG_FNAME_IND$iname"
         } else {
            # if catalog is not active, use same name
            set tmp_oname $iname
         }
      }

      if {![zcreate $iname]} {
         db_io_code_log $mname "exit:cannot create table" \
                        $DBENG_CANNOT_CREATE_TABLE
         set ret $DBENG_CANNOT_CREATE_TABLE
      } else {
         if {$systable_flag} {
            db_io_code_log $mname "exit:created system table" $DBENG_OK
         } else {
            db_io_code_log $mname "exit:created table" $DBENG_OK
         }

         set ret $DBENG_OK
      }

      set uoname $tmp_oname
      return $ret
   }
            
   # both logical and physical file names given, separate the names
   if {![ll_word $iname lname 1 ","]} {
      db_io_code_log $mname "bad rc(0) from ll_word(lname)" \
                     $DBENG_INTERNAL_ERROR
      return $DBENG_INTERNAL_ERROR
   }

   if {![ll_word $iname pname 2 ","]} {
      db_io_code_log $mname "bad rc(0) from ll_word(pname)" \
                     $DBENG_INTERNAL_ERROR
      return $DBENG_INTERNAL_ERROR
   }

   # attempt to create the new table file using the physical file name
   if {![zcreate $pname]} {
      db_io_code_log $mname "exit:error creating empty table" \
                     $DBENG_CANNOT_CREATE_TABLE
      return $DBENG_CANNOT_CREATE_TABLE
   }

   # attmept to create the catalog entry
   set ret [dbeng_catalog_new $lname $pname]

   if {$ret != $DBENG_OK} {
      db_io_code_log $mname "bad rc from dbeng_catalog_new" $ret
      file delete $pname
      return $ret
   }

   if {$catalog_flag} {
      # output logical name if catalog is on
      set tmp_oname $lname
   } else {
      # output physical file name (no indicator) if catalog is off
      set tmp_oname $pname
   }

   logman "$mname:normal exit(0),oname=$tmp_oname,l=[string length $tmp_oname]"
   set uoname $tmp_oname
   return $DBENG_OK
}

proc dbeng_catalog_delete {tname} {
   # Delete the catalog entry with the table name 'tname'.
   # Returns a 'dbeng' code.
   global cat DBENG_OK DBENG_INVALID_FUNCTION
   set mname "dbeng_catalog_delete"

   logman "$mname:enter"

   if {$tname == ""} {
      db_io_code_log $mname "exit:empty(tname)" $DBENG_INVALID_FUNCTION
      return $DBENG_INVALID_FUNCTION
   }

   # open catalog
   set ret [dbeng_catalog_open]

   if {$ret != $DBENG_OK} {
      db_io_code_log $mname "exit:bad ret from dbeng_catalog_open" $ret
      return $ret
   }

   # find table name in catalog
   set ret [dbeng_catalog_tfind $tname]

   if {$ret != $DBENG_OK} {
      db_io_code_log $mname "exit:catalog entry not found" $ret
      dbeng_catalog_close
      return $ret
   }

   set ret [db_delete $cat]

   if {$ret != $DBENG_OK} {
      db_io_code_log $mname "exit:bad ret from db_delete" $ret
      dbeng_catalog_close
      return $ret
   }

   set ret [dbeng_catalog_close]

   if {$ret != $DBENG_OK} {
      db_io_code_log $mname "exit:bad ret from dbeng_catalog_close" $ret
      return $ret
   }

   db_io_code_log $mname "normal exit" $DBENG_OK
   return $DBENG_OK
}

proc dbeng_catalog_table_details {tname systable_flag} {
   # Find a catalog entry by its logical table name and load all
   # catalog details into the array 'dbengcat_details'.
   # Returns a 'dbeng' code.
   global cat dbengcat_details DBENG_OK DBENG_INVALID_FUNCTION
   global DBENG_CATALOG_FNAME_IND DBENG_CATALOG_FIELD_FNAME
   global DBENG_CATALOG_FIELD_AUTOPACK DBENG_NO_SUCH_FIELD
   set mname "dbeng_catalog_table_details"

   logman "$mname:enter"
   set dbengcat_details(physical_name) ""
   set dbengcat_details(apack) 0

   if {$tname == ""} {
      db_io_code_log $mname "empty(tname)" $DBENG_INVALID_FUNCTION
      return $DBENG_INVALID_FUNCTION
   }
      
   if {![num $systable_flag]} {
      db_io_code_log $mname "not numeric(systable_flag)" \
                     $DBENG_INVALID_FUNCTION
      return $DBENG_INVALID_FUNCTION
   }

   if {$systable_flag != 1 && $systable_flag != 0} {
      db_io_code_log $mname "not 0|1(systable_flag)" $DBENG_INVALID_FUNCTION
      reutrn $DBENG_INVALID_FUNCTION
   }

   db_config_get_catalog_flag catalog_flag

   # resolve supplied table name (could be logical or physical)
   # to the physical OS path/file name
   # if its a system table, copy name
   if {$systable_flag} {
      set dbengcat_details(physical_name) $tname
      db_io_code_log $mname "exit:found system table" $DBENG_OK
      return $DBENG_OK
   }

   # if the catalog is not on, copy name
   if {!$catalog_flag} {
      set dbengcat_details(physical_name) $tname
      db_io_code_log $mname "exit:catalog is not on" $DBENG_OK
      return $DBENG_OK
   }
      
   # if the name starts with the file name indicator, copy
   # the same name minus the indicator
   if {[string index $tname 0] == $DBENG_CATALOG_FNAME_IND} {
      set dbengcat_details(physical_name) [string range $tname 1 end]
      db_io_code_log $mname "exit:found physical file name" $DBENG_OK
      return $DBENG_OK
   }

   # the only other possibility is a catalog table name which we
   # lookup and get the catalog details
   if {$cat != 0} {
      db_io_code_log $mname "exit:catalog already open" $DBENG_INVALID_FUNCTION
      return $DBENG_INVALID_FUNCTION
   }

   set ret [dbeng_catalog_open]

   if {$ret != $DBENG_OK} {
      db_io_code_log $mname "exit:bad ret from dbeng_catalog_open" $ret
      return $ret
   }

   # find catalog entry by its logical name
   set ret [dbeng_catalog_tfind $tname]

   if {$ret != $DBENG_OK} {
      db_io_code_log $mname "exit:bad rc from dbeng_catalog_tfind" $ret
      dbeng_catalog_close
      return $ret
   }

   # load physical file name
   set ret [db_get_field $cat $DBENG_CATALOG_FIELD_FNAME tmp]

   if {$ret != $DBENG_OK} {
      db_io_code_log $mname "exit:bad rc from db_get_field" $ret
      dbeng_catalog_close
      return $ret
   }

   set dbengcat_details(physical_name) $tmp

   # load autopack value (ok if not present)
   set ret [db_get_field $cat $DBENG_CATALOG_FIELD_AUTOPACK tmp]

   if {$ret != $DBENG_OK} {
      if {$ret == $DBENG_NO_SUCH_FIELD} {
         set dbengcat_details(apack) 0
         set ret $DBENG_OK
      } else {
         db_io_code_log $mname "exit:bad rc from db_get_field" $ret
         dbeng_catalog_close
         return $ret
      }
   } else {
      if {![num $tmp]} {
         db_io_code_log $mname "exit:autopack value not a number" \
                        $BDNEG_INVALID_FUNCTION
         dbeng_catalog_close
         return $DBENG_INVALID_FUNCTION
      }

      set dbengcat_details(apack) $tmp
   }


   dbeng_catalog_close
   db_io_code_log $mname "normal exit" $ret
   return $ret
}

proc dbeng_catalog_fname {tname fname} {
   # Given a table name ('tname'), locate and return the physical
   # name ('fname') from the systam catalog. Upon success, the
   # physical file name will be loaded into 'fname'. Note that
   # this procedure assumes the catalog is currently closed.
   # Returns a 'dbeng' code.
   global cat DBENG_INVALID_FUNCTION DBENG_OK DBENG_CATALOG_FIELD_FNAME
   upvar $fname ufname
   set mname "dbeng_catalog_fname"

   logman "$mname:enter"

   if {$tname == ""} {
      db_io_code_log $mname "exit:empty(tname)" $DBENG_INVALID_FUNCTION
      return $DBENG_INVALID_FUNCTION
   }

   if {$cat} {
      db_io_code_log $mname "exit:catalog already open" \
                     $DBENG_INVALID_FUNCTION
      return $DBENG_INVALID_FUNCTION
   }

   set ufname ""
   set ret [dbeng_catalog_open]

   if {$ret != $DBENG_OK} {
      db_io_code_log $mname "exit:bad rc from dbeng_catalog_open" $ret
      return $ret
   }

   # find table name in catalog
   set ret [dbeng_catalog_tfind $tname]

   if {$ret != $DBENG_OK} {
      db_io_code_log $mname "exit:bad rc from dbeng_catalog_tfind" $ret
      dbeng_catalog_close
      return $ret
   }

   # get physical file name
   set ret [db_get_field $cat $DBENG_CATALOG_FIELD_FNAME tmp]

   if {$ret != $DBENG_OK} {
      db_io_code_log $mname "exit:bad rc from db_get_field" $ret
      dbeng_catalog_close
      return $ret
   }

   set ufname $tmp
   set ret [dbeng_catalog_close]

   if {$ret != $DBENG_OK} {
      db_io_code_log $mname "exit:bad rc from dbeng_catalog_close" $ret
      return $ret
   }

   db_io_code_log $mname "normal exit" $DBENG_OK
   return $DBENG_OK
}

proc db_get_catalog_list {pat list_out} {
   # Return a list of matching table names (logical) from
   # the system catalog. List is returned in 'list_out'.
   # The list is delimited by 'DBENG_LIST_DELIM'. Each
   # entry in the list contains the logical name
   # followed by the file/path name (physical name). The two
   # fields in the list are delimited by a comma.
   # The parameter 'pat' may contain a pattern
   # acceptable to the tcl 'string match' command.
   # If 'pat' is empty, all entries from the catalog
   # will be returned. Returns a 'dbeng' code.
   global cat DBENG_OK DBENG_CATALOG_FIELD_TNAME DBENG_CATALOG_FIELD_FNAME
   global DBENG_LIST_DELIM DBENG_INTERNAL_ERROR
   upvar $list_out ulist_out
   set mname "dbeng_catalog_list"

   logman "$mname:enter"

   if {$pat == ""} {
      set is_pat 0
   } else {
      set is_pat 1
   }

   set ulist_out ""
   set ret [dbeng_catalog_open]

   if {$ret != $DBENG_OK} {
      db_io_code_log $mname "exit:bad rc from dbeng_catalog_open" $ret
      return $ret
   }

   # get the number of records
   set ret [db_get_rec_count $cat rec_cnt dummy]

   if {$ret != $DBENG_OK} {
      db_io_code_log $mname "exit:bad rc from db_get_rec_count" $ret
      return $ret
   }

   # it is not an error if there are no records
   if {$rec_cnt == 0} {
      db_io_code_log $mname "exit:list is empty" $DBENG_OK
      dbeng_catalog_close
      return $DBENG_OK
   }

   set before ""
   set after ""
   set cnt 1
   set ret [db_top $cat]
 
   if {$ret != $DBENG_OK} {
      db_io_code_log $mname "exit:error going top of catalog" $ret
      dbeng_catalog_close
      return $ret
   }

   # get first record
   set ret [db_next $cat]

   if {$ret != $DBENG_OK} {
      db_io_code_log $mname "exit:error getting first record" $ret
      dbeng_catalog_close
      return $ret
   }

   while {$ret == $DBENG_OK} {
      set ret [db_get_field $cat $DBENG_CATALOG_FIELD_TNAME tname]

      if {$ret != $DBENG_OK} {
         db_io_code_log $mname "exit:bad rc from db_get_field(tname)" $ret
         dbeng_catalog_close
         return $ret
      }

      set process 1

      # match to pattern (if supplied)
      if {$is_pat} {
         if {![string match $pat $tname]} {
            set process 0
         }
      }

      if {$process} {
         set ret [db_get_field $cat $DBENG_CATALOG_FIELD_FNAME fname]
 
         if {$ret != $DBENG_OK} {
            db_io_code_log $mname "exit:bad rc from db_get_field(fname)" $ret
            dbeng_catalog_close
            return $ret
         }

         set wdata "$tname,$fname"

         if {![ll_wordput $before after $wdata $cnt $DBENG_LIST_DELIM]} { 
            db_io_code_log $mname "exit:error putting list item" \
                           $DBENG_INTERNAL_ERROR
            dbeng_catalog_close
            return $DBENG_INTERNAL_ERROR
         }

         incr cnt
         set before $after
      }

      # get next record
      set ret [db_next $cat]
   }

   dbeng_catalog_close
   set ulist_out $after
   db_io_code_log $mname "normal exit" $DBENG_OK
   return $DBENG_OK
}   

proc dbeng_catalog_tfind {tname} {
   # Locate the table by table name ('tname') within the
   # system catalog. The system catalog is assumed to be
   # already open. It is an error if the catalog is found
   # to be closed. Returns a 'dbeng' code.
   global cat DBENG_OK DBENG_INVALID_FUNCTION DBENG_CATALOG_FIELD_TNAME
   global DBENG_EOF DBENG_NO_CATALOG_ENTRY
   set mname "dbeng_catalog_tfind"

   logman "$mname:enter"

   if {$tname == ""} {
      db_io_code_log $mname "exit:empty(tname)" $DBENG_INVALID_FUNCTION
      return $DBENG_INVALID_FUNCTION
   }

   if {$cat == 0} {
      db_io_code_log $mname "exit:catalog not open" $DBENG_INVALID_FUNCTION
      return $DBENG_INVALID_FUNCTION
   }
   
   set ret [db_top $cat]

   if {$ret != $DBENG_OK} {
      db_io_code_log $mname "exit:bad ret from db_top" $ret
      return $ret
   }

   if {[db_find_field $cat 0 $tname $DBENG_CATALOG_FIELD_TNAME] == \
       $DBENG_EOF} {
      set ret $DBENG_NO_CATALOG_ENTRY
   }

   db_io_code_log $mname "normal exit" $ret
   return $ret
}

proc dbeng_catalog_open {} {
   # Open the catalog table and load global 'cat' table ID.
   # Returns a 'dbeng' code.
   global DBENG_INVALID_FUNCTION DBENG_OK DBENG_UNABLE_TO_OPEN
   global cat
   set mname "dbeng_catalog_open"

   logman "$mname:enter"

   if {$cat > 0} {
      db_io_code_log $mname "exit:catalog already open" $DBENG_INVALID_FUNCTION
      return $DBENG_INVALID_FUNCTION
   }

   # get catalog name
   set ret [db_config_get_catalog cat_table]

   if {$ret != $DBENG_OK} {
      db_io_code_log $mname "exit:error getting catalog name" $ret
      return $ret
   }

   set ret [dbeng_open_systable $cat_table tid]

   if {$ret != $DBENG_OK} {
      db_io_code_log $mname "exit:error opening catalog" $ret
      return $ret
   }

   set cat $tid
   db_io_code_log $mname "normal exit" $DBENG_OK
   return $DBENG_OK
}

proc dbeng_catalog_close {} {
   # Close the system catalog table. Returns a 'dbeng' code.
   global cat DBENG_NO_SUCH_FILE
   set mname "dbeng_catalog_close"

   logman "$mname:enter"

   if {$cat == 0} {
      db_io_code_log $mname "exit:catalog table not open" \
                     $DBENG_NO_SUCH_FILE
      return $DBENG_NO_SUCH_FILE
   }

   set ret [db_close $cat]
   set cat 0
   db_io_code_log $mname "normal exit" $ret
   return $ret
}
