#!/usr/bin/mawk -We
# =====================================================================
# constraint: experimental NoSQL Referential Integrity Schema processor.
#
# Copyright (c) 2006,2007,2008 Carlo Strozzi
#
# 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; version 2 dated June, 1991.
#
# 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, write to the Free Software
# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
#
# =====================================================================
#
# Sample input on STDIN:
#
#   Column     |Table       |Value     |Action
#   -----------|------------|----------|------
#   Acct       |saleitem    |3050      |Insert
#   Agent      |opportunity |carlo     |Insert
#   Nick       |nick        |goofy     |Delete
#   Ref        |journal-2006|pi.2006.18|Insert
#   Ref        |ref         |pi.2006.17|Delete
#   Ref        |rel         |pi.2006.17|Insert
#   Sale       |saleitem    |2005_19   |Delete
#   Sale       |saleorder   |2005_99   |Delete
#   comune_prov|comune      |ZZ        |Insert
#   comune_prov|provincia   |RR        |Delete
#
# =====================================================================
# $Id: constraint 51 2009-08-03 15:56:09Z carlo $

BEGIN {
  NULL = ""; FS = OFS = "\t"; pfx = ""

  # Get local settings.
  nosql_install = ENVIRON["NOSQL_INSTALL"]
  stdout = ENVIRON["NOSQL_STDOUT"]
  stderr = ENVIRON["NOSQL_STDERR"]

  # Set default values if necessary.
  if (nosql_install == NULL) nosql_install = "/usr/local/nosql"
  if (stdout == NULL) stdout = "/dev/stdout"
  if (stderr == NULL) stderr = "/dev/stderr"

  # Process command-line options and arguments.

  while (ARGV[++i] != NULL) {
    if (ARGV[i] == "-i" || ARGV[i] == "--input") i_file = ARGV[++i]
    else if (ARGV[i] == "-o" || ARGV[i] == "--output") o_file = ARGV[++i]
    else if (ARGV[i] == "-I" || ARGV[i] == "--initial-tables") {
       t = split(ARGV[++i],tables,",")
    }
    else if (ARGV[i] == "-Q" || ARGV[i] == "--quoting-style") quote = ARGV[++i]
    else if (ARGV[i] == "-P" || ARGV[i] == "--prefix") pfx = ARGV[++i]
    else if (ARGV[i] == "-p" || ARGV[i] == "--path") path = ARGV[++i]
    else if (ARGV[i] == "-l" || ARGV[i] == "--lock-command") lcmd = ARGV[++i]
    else if (ARGV[i] == "-L" || ARGV[i] == "--lock-only") locks_only = 1
    else if (ARGV[i] == "-T" || ARGV[i] == "--test-only") tests_only = 1
    else if (ARGV[i] == "-q" || ARGV[i] == "--quiet") quiet = 1
    else if (ARGV[i] == "-e" || ARGV[i] == "--lock-error-handler") {
       lerr = ARGV[++i]
    }
    else if (ARGV[i] == "-s" || ARGV[i] == "--lock-suffix") {
       lsuf = ARGV[++i]
       if (lsuf == "") lsuf = "--NONE--"
    }
    else if (ARGV[i] == "-a" || ARGV[i] == "--lock-also") {
       lother = ARGV[++i]
    }
    else if (ARGV[i] == "-h") {
       system("grep -v '^#' " nosql_install "/help/constraint.txt")
       exit(rc=1)
    }
    else if (ARGV[i] == "--show-copying") {
       system("cat " nosql_install "/doc/COPYING")
       exit(rc=1)
    }
    else if (ARGV[i] == "--show-warranty") {
       system("cat " nosql_install "/doc/WARRANTY")
       exit(rc=1)
    }
    else if (s_table !~ /^-/) s_table = ARGV[i]
  }

  ARGC = 1					# Fix argv[]

  if (o_file == NULL) o_file = stdout
  if (i_file != NULL) { ARGV[1] = i_file; ARGC = 2 }

  # Prime the output script; default quoting-style is "sh".
  # This must come before any "exit(rc=1)", except those triggered
  # by the option processing loop above.

  if (quote == "rc") script_first = "status=0"
  else script_first = "true"

  if (s_table == "") {
     print "usage: constraint [options] column-schema" > stderr
     exit(rc=1)
  }

  if (path != "") {
     sub(/\/*$/,"/",path)		# sanitize path.

     # prepend path to schema if necessary.
     if (s_table !~ /^\//) s_table = path "/" s_table
  }

  # set defaults.
  if (lcmd == "") lcmd = "lockfile -r0"

  if (lsuf == "") lsuf = ".lock"
  else if (lsuf == "--NONE--") lsuf = ""

  # '-L' overrides '-T'.
  if (tests_only && locks_only) tests_only = 0

  # Load schema header into memory
  i=0
  if ((getline < s_table) <= 0) {
     print "contraint: could not read schema table '"s_table"'" > stderr
     exit(rc=1)
  }

  # Remove SOH markers.
  gsub(/\001/, "")

  # Load the column position array.
  i=0; while (++i <= NF) p[$i] = i

  # check for required fields.
  if (!p[pfx "Column"] || !p[pfx "Table"] || !p[pfx "Flags"] || \
	!p[pfx "Parent"] || !p[pfx "Insert"] || !p[pfx "Delete"]) {
     print "constraint: missing field(s) in schema table '"s_table"'" > stderr
     exit(rc=1)
  }

  # load schema body.

  while ((getline < s_table) > 0) {
    col = $(p[pfx "Column"])
    tbl = $(p[pfx "Table"])

    # attribute name defaults to column name.

    if (p[pfx "Aliasof"] < 1 || $(p[pfx "Aliasof"]) == "")
						Attrib[col,tbl] = col
    else Attrib[col,tbl] = $(p[pfx "Aliasof"])

    pcol = Attrib[col,tbl]			# parent column name.

    Parent[col,tbl] = $(p[pfx "Parent"])
    Insert[col,tbl] = $(p[pfx "Insert"])
    Delete[col,tbl] = $(p[pfx "Delete"])
    Flags[col,tbl] = $(p[pfx "Flags"])

    # Key attribute implies not-null, so I add the "N" flag, although
    # it may already be there, but it won't hurt having it multiple times,
    # since flags are supposed to be repeatable. Setting a key not-null
    # is redundant, as this program is only concerned with key fields,
    # be they primary or foreign keys, but being explicit will not do
    # any harm, and it may turn out to be useful should this program
    # eventually be extended to handle also non-key fields.

    if (Flags[col,tbl] ~/[kK]/) Flags[col,tbl] = Flags[col,tbl] "N"

    if (Parent[col,tbl] != "" && tolower(Insert[col,tbl]) == "dependent") {
       Childs[pcol,Parent[col,tbl]] = Childs[pcol,Parent[col,tbl]] "\001" tbl
       Target[pcol,tbl,++tgt] = col
    }
  }

  close(s_table);

  NR = 0				# reset, just in case.
}

# Read input table.

NR == 1 {				# table header.
   gsub(/\001/, "")			# remove SOH markers.

   # Load the column position array.
   i=0; while (++i <= NF) P[$i] = i

   # check for required fields.
   if (!P["Column"] || !P["Table"] || !P["Value"] || !P["Action"]) {
      print "constraint: missing field(s) in input table" > stderr
      exit(rc=1)
   }
   next
}

# Handle null key value in input. Again, being this program only concerned
# with keys, testing for null would suffice, but testing also for flag
# "N" may become useful in the future, and it does not hurt.

$(P["Value"]) == "" && Flags[$(P["Column"]),$(P["Table"])] ~ /N/ { 
   print "constraint: null input value for table '" \
	$(P["Table"]) "', column '" $(P["Column"]) "'" > stderr
   exit(rc=1)
}

# Handle "On-Insert" constraint.

$(P["Action"]) == "Insert" {

   child = tables[++t] = $(P["Table"])
   field = $(P["Column"])

   # Mark child as inserted in insert list.
   idx = child "\t" field "\t" $(P["Value"])

   if (NeedTest[idx] != "" && NeedTest[idx] != "inserted" && \
	 			NeedTest[idx] != "insert needtest") {
      print "constraint: conflicting directives in input" > stderr
      exit(rc=1)
   }

   NeedTest[idx] = "inserted"

   if (Parent[field,child] != "") {

      # Mark the need for parent insertion.
      idx = Parent[field,child] "\t" field "\t" $(P["Value"])

      if (NeedTest[idx] != "" && NeedTest[idx] != "inserted" && \
	 NeedTest[idx] != "insert needtest") {
	 print "constraint: conflicting directives in input" > stderr
	 exit(rc=1)
      }

      if (NeedTest[idx] != "inserted") NeedTest[idx] = "insert needtest"

      if (tolower(Insert[field,child]) == "dependent") {

         parent_field = Attrib[field,child]

         tables[++t] = Parent[field,child]

         # linear search on 1st field (flag=s).
         if (Flags[parent_field,Parent[field,child]] ~ /s/)
	    script["Insert\t" idx] = sprintf("grep -wq %s %s%s",\
			escape("^" $(P["Value"]),quote),\
			path,Parent[field,child])

         # binary search on 1st field (flag=b).
         else if (Flags[parent_field,Parent[field,child]] ~ /b/)
	    script["Insert\t" idx] = sprintf("keysearch -t %s %s%s",\
			escape($(P["Value"]),quote),\
			path,Parent[field,child])

         # insert more access methods here as needed.

         # default access method is linear search by column name (flag=S).
         else {
	    awk_pgm = "BEGIN{_RC=1}$" parent_field "==" \
			escape($(P["Value"]),"awk") "{exit(_RC=0)}" \
			"END{exit(_RC)}"
	    script["Insert\t" idx] = sprintf("awktable -i %s%s -- %s",\
			path,Parent[field,child],escape(awk_pgm,quote))
         }
      }
   }
}

# Handle "On-Delete" constraint.

$(P["Action"]) == "Delete" {

   parent = tables[++t] = $(P["Table"])
   field = $(P["Column"])

   # Mark parent as deleted in deletion list.
   idx = parent "\t" field "\t" $(P["Value"])

   if (NeedTest[idx] != "" && NeedTest[idx] != "deleted" && \
				NeedTest[idx] != "delete needtest") {
      print "constraint: conflicting directives in input" > stderr
      exit(rc=1)
   }

   NeedTest[idx] = "deleted"

   if (tolower(Delete[field,parent]) == "restrict") {

      sub(/^\001/,"",Childs[field,parent])
      i = split(Childs[field,parent],a,"\001")
      sort(a,i)
      j = uniq(a,i)

      for (i=1; i<=j; i++) {

	 tables[++t] = a[i]

	 for (k=1; k<=tgt; k++) {

	     Target[field,a[i],k] == "" ? \
		child_field = field : child_field = Target[field,a[i],k]

	     # Mark child as to be deleted in test list, if necessary.
	     idx = a[i] "\t" child_field "\t" $(P["Value"])

	     if (NeedTest[idx] != "" && NeedTest[idx] != "deleted" && \
					NeedTest[idx] != "delete needtest") {
		print "constraint: conflicting directives in input" > stderr
		exit(rc=1)
	     }

	     if (NeedTest[idx] != "deleted") NeedTest[idx] = "delete needtest"

	     # linear search on 1st field (flag=s).
	     if (Flags[child_field,a[i]] ~ /s/)
		    script["Delete\t" idx] = sprintf("! grep -wq %s %s%s",\
			    escape("^" $(P["Value"]),quote),path,a[i])

	     # binary search on 1st field (flag=b).
	     else if (Flags[child_field,a[i]] ~ /b/)
		    script["Delete\t" idx] = sprintf("! keysearch -t %s %s%s",\
			    escape($(P["Value"]),quote),path,a[i])

	     # insert more access methods here as needed.

	     # default access method is linear search by column name (flag=S).
	     else {
		  awk_pgm = "BEGIN{_RC=1}$" child_field "==" \
			    escape($(P["Value"]),"awk") "{exit(_RC=0)}" \
			    "END{exit(_RC)}"
		  script["Delete\t" idx] = sprintf("! awktable -i %s%s -- %s",\
			    path,a[i],escape(awk_pgm,quote))
	     }
	 }
      }
   }
}

END {

  if (rc) {
     # Make sure the error is triggered also in the resulting script.
     if (o_file != "") print "false" > o_file
     exit(rc)
  }

  # append any extra files to lock-list.

  if (lother != "") {
     i = split(lother,a,",")
     while (i) tables[++t] = a[i--]
  }

  if (!tests_only && t) {
     sort(tables,t)
     t = uniq(tables,t)
     for (i=1; i<=t; i++) {
	 if (i==1) printf("%s",lcmd) > o_file
	 if (tables[i] == "") continue			# just in case.
	 # prepend path only if not already in full-path.
	 if (tables[i] !~ /^\//)
	    printf(" %s%s%s",path,tables[i],lsuf) > o_file
	 else printf(" %s%s",tables[i],lsuf) > o_file
     }

     if (lerr != "") printf(" || %s",lerr) > o_file
     printf("\n") > o_file
  }

  if (!locks_only) {

     printf("%s", script_first) > o_file

     for (idx in NeedTest) {
	 if (NeedTest[idx] == "insert needtest" && \
			script["Insert\t" idx] != "") {
	    if (!quiet) print "constraint: insert test triggered" > stderr
	    printf(" &&\n%s",script["Insert\t" idx]) > o_file
	 }
	 else if (NeedTest[idx] == "delete needtest" && \
				script["Delete\t" idx] != "") {
	    if (!quiet) print "constraint: delete test triggered" > stderr
	    printf(" &&\n%s",script["Delete\t" idx]) > o_file
	 }
     }

     printf("\n") > o_file
  }
}

# Program functions.

# sort an array.
function sort(a,e,			tmp,i,j) {

   for (i=2; i <= e; ++i) {
       for (j=i; a[j-1] > a[j]; --j) {
           tmp = a[j]
           a[j] = a[j-1]
           a[j-1] = tmp
       }
   }
   return
}

# discard repeated values from a sorted array; return new array length.
function uniq(a,e,			b,i,j) {

   while (++i <= e) b[i] = a[i]		# copy array to workarea.
   j=1
   for (i=2; i <= e; ++i)
       if (b[i] != b[i-1]) a[++j] = b[i]
   return j
}

function escape(string,type) {

  if (type == "rc") {
     gsub("'", "''", string)
     return "'" string "'"
  }

  if (type == "awk") {
     gsub(/\\/, "&&",string)
     gsub(/"/, "\\\"", string)
     gsub(/\n/, "\\n", string)
     return "\"" string "\""
  }

  else {
     # default quoting-style is "sh".
     gsub(/\\/, "&&",string)
     gsub(/\$/, "\\$",string)
     gsub(/`/, "\\`",string)
     gsub(/"/,"\\\"",string)
     return "\"" string "\""
  }
}

# End of program.
