Index: trunk/DNSDB.pm
===================================================================
--- trunk/DNSDB.pm	(revision 542)
+++ trunk/DNSDB.pm	(revision 543)
@@ -31,4 +31,5 @@
 use POSIX;
 use Fcntl qw(:flock);
+use Time::TAI64 qw(:tai64);
 
 use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
@@ -3536,8 +3537,9 @@
   my $id = shift;
 
+##fixme: do we need a knob to twist to switch between unix epoch and postgres time string?
   my $sql = "SELECT record_id,host,type,val,ttl".
 	($defrec eq 'n' ? ',location' : '').
 	($revrec eq 'n' ? ',distance,weight,port' : '').
-	(($defrec eq 'y') ? ',group_id FROM ' : ',domain_id,rdns_id FROM ').
+	(($defrec eq 'y') ? ',group_id FROM ' : ',domain_id,rdns_id,stamp,stamp < now() AS ispast,expires,stampactive FROM ').
 	_rectable($defrec,$revrec)." WHERE record_id=?";
   my $ret = $dbh->selectrow_hashref($sql, undef, ($id) );
@@ -3608,6 +3610,8 @@
   $newsort =~ s/^,//;
 
+##fixme:  do we need a knob to twist to switch from unix epoch to postgres time string?
   my $sql = "SELECT r.record_id,r.host,r.type,r.val,r.ttl";
-  $sql .= ",l.description AS locname" if $args{defrec} eq 'n';
+  $sql .= ",l.description AS locname,stamp,r.stamp < now() AS ispast,r.expires,r.stampactive"
+	if $args{defrec} eq 'n';
   $sql .= ",r.distance,r.weight,r.port" if $args{revrec} eq 'n';
   $sql .= " FROM "._rectable($args{defrec},$args{revrec})." r ";
@@ -3627,4 +3631,5 @@
   my $ret = $dbh->selectall_arrayref($sql, { Slice => {} }, (@bindvars) );
   $errstr = "Error retrieving records: ".$dbh->errstr if !$ret;
+
   return $ret;
 } # end getRecList()
@@ -3685,4 +3690,10 @@
   my $location = shift;
   $location  = '' if !$location;
+
+  my $expires = shift;
+  $expires = 1 if $expires eq 'until';	# Turn some special values into the appropriate booleans.
+  $expires = 0 if $expires eq 'after';
+  my $stamp = shift;
+  $stamp = '' if !$stamp;	 # Timestamp should be a string at this point.
 
   # Spaces are evil.
@@ -3745,9 +3756,19 @@
   push @vallist, ($$host,$$rectype,$$val,$ttl,$id);
 
-  # locations are not for default records, silly coder!
   if ($defrec eq 'n') {
+    # locations are not for default records, silly coder!
     $fields .= ",location";
     push @vallist, $location;
-  }
+    # timestamps are rare.
+    if ($stamp) {
+      $fields .= ",stamp,expires,stampactive";
+      push @vallist, $stamp, $expires, 'y';
+    } else {
+      $fields .= ",stampactive";
+      push @vallist, 'n';
+    }
+  }
+
+  # a little magic to get the right number of ? placeholders based on how many values we're providing
   my $vallen = '?'.(',?'x$#vallist);
 
@@ -3777,4 +3798,5 @@
   $logdata{entry} .= "', TTL $ttl";
   $logdata{entry} .= ", location ".$self->getLoc($location)->{description} if $location;
+  $logdata{entry} .= ($expires eq 'after' ? ', valid after ' : ', expires at ').$stamp if $stamp;
 
   # Allow transactions, and raise an exception on errors so we can catch it later.
@@ -3830,4 +3852,10 @@
   $location  = '' if !$location;
 
+  my $expires = shift;
+  $expires = 1 if $expires eq 'until';	# Turn some special values into the appropriate booleans.
+  $expires = 0 if $expires eq 'after';
+  my $stamp = shift;
+  $stamp = '' if !$stamp;	 # Timestamp should be a string at this point.
+
   # just set it to an empty string;  failures will be caught later.
   $$host = '' if !$$host;
@@ -3901,8 +3929,16 @@
 	($defrec eq 'y' ? $oldrec->{group_id} : ($revrec eq 'n' ? $oldrec->{domain_id} : $oldrec->{rdns_id})) );
 
-  # locations are not for default records, silly coder!
   if ($defrec eq 'n') {
+    # locations are not for default records, silly coder!
     $fields .= ",location";
     push @vallist, $location;
+    # timestamps are rare.
+    if ($stamp) {
+      $fields .= ",stamp,expires,stampactive";
+      push @vallist, $stamp, $expires, 'y';
+    } else {
+      $fields .= ",stampactive";
+      push @vallist, 'n';
+    }
   }
 
@@ -3958,4 +3994,6 @@
   $logdata{entry} .= "', TTL $oldrec->{ttl}";
   $logdata{entry} .= ", location ".$self->getLoc($oldrec->{location})->{description} if $oldrec->{location};
+  $logdata{entry} .= ($oldrec->{expires} ? ', expires at ' : ', valid after ').$oldrec->{stamp}
+	if $oldrec->{stampactive};
   $logdata{entry} .= "\nto\n";
   # More NS special
@@ -3969,4 +4007,5 @@
   $logdata{entry} .= "', TTL $ttl";
   $logdata{entry} .= ", location ".$self->getLoc($location)->{description} if $location;
+  $logdata{entry} .= ($expires eq 'after' ? ', valid after ' : ', expires at ').$stamp if $stamp;
 
   local $dbh->{AutoCommit} = 0;
@@ -4975,5 +5014,5 @@
   my $soasth = $dbh->prepare("SELECT host,type,val,distance,weight,port,ttl,record_id,location ".
 	"FROM records WHERE rdns_id=? AND type=6");
-  my $recsth = $dbh->prepare("SELECT host,type,val,distance,weight,port,ttl,record_id,location ".
+  my $recsth = $dbh->prepare("SELECT host,type,val,distance,weight,port,ttl,record_id,location,extract(epoch from stamp),expires,stampactive ".
 	"FROM records WHERE rdns_id=? AND not type=6 ".
 	"ORDER BY masklen(CAST(val AS inet)) DESC, CAST(val AS inet)");
@@ -5008,24 +5047,19 @@
 
         $recsth->execute($revid);
-        while (my ($host,$type,$val,$dist,$weight,$port,$ttl,$recid,$loc) = $recsth->fetchrow_array) {
+        while (my ($host,$type,$val,$dist,$weight,$port,$ttl,$recid,$loc,$stamp,$expires,$stampactive) = $recsth->fetchrow_array) {
           next if $recflags{$recid};
 
-          $loc = '' if !$loc;	# de-nullify - just in case
-##fixme:  handle case of record-with-location-that-doesn't-exist better.
-# note this currently fails safe (tested) - records with a location that
-# doesn't exist will not be sent to any client
-#	$loc = '' if !$lochash->{$loc};
-
-##fixme:  record validity timestamp. tinydns supports fiddling with timestamps.
-# note $ttl must be set to 0 if we want to use tinydns's auto-expiring timestamps.
-# timestamps are TAI64
-# ~~ 2^62 + time()
-          my $stamp = '';
-
-          # support tinydns' auto-TTL
-          $ttl = '' if $ttl == -1;
+# not sure this is necessary for revzones.
+#	  # Spaces are evil.
+#	  $val =~ s/^\s+//;
+#	  $val =~ s/\s+$//;
+#	  if ($typemap{$type} ne 'TXT') {
+#	    # Leading or trailng spaces could be legit in TXT records.
+#	    $host =~ s/^\s+//;
+#	    $host =~ s/\s+$//;
+#	  }
 
           _printrec_tiny(*ZONECACHE, 'y', \%recflags, $revzone,
-            $host, $type, $val, $dist, $weight, $port, $ttl, $loc, $stamp)
+            $host, $type, $val, $dist, $weight, $port, $ttl, $loc, $stamp, $expires, $stampactive)
 		if *ZONECACHE;
 
@@ -5062,5 +5096,5 @@
 
   my $domsth = $dbh->prepare("SELECT domain_id,domain,status,changed FROM domains WHERE status=1");
-  $recsth = $dbh->prepare("SELECT host,type,val,distance,weight,port,ttl,record_id,location ".
+  $recsth = $dbh->prepare("SELECT host,type,val,distance,weight,port,ttl,record_id,location,extract(epoch from stamp),expires,stampactive ".
 	"FROM records WHERE domain_id=?");	# Just exclude all types relating to rDNS
 #	"FROM records WHERE domain_id=? AND type < 65280");	# Just exclude all types relating to rDNS
@@ -5083,21 +5117,6 @@
 
         $recsth->execute($domid);
-        while (my ($host,$type,$val,$dist,$weight,$port,$ttl,$recid,$loc) = $recsth->fetchrow_array) {
+        while (my ($host,$type,$val,$dist,$weight,$port,$ttl,$recid,$loc,$stamp,$expires,$stampactive) = $recsth->fetchrow_array) {
 	  next if $recflags{$recid};
-
-	  $loc = '' if !$loc;	# de-nullify - just in case
-##fixme:  handle case of record-with-location-that-doesn't-exist better.
-# note this currently fails safe (tested) - records with a location that
-# doesn't exist will not be sent to any client
-#	$loc = '' if !$lochash->{$loc};
-
-##fixme:  record validity timestamp. tinydns supports fiddling with timestamps.
-# note $ttl must be set to 0 if we want to use tinydns's auto-expiring timestamps.
-# timestamps are TAI64
-# ~~ 2^62 + time()
-	  my $stamp = '';
-
-	  # support tinydns' auto-TTL
-	  $ttl = '' if $ttl == -1;
 
 	  # Spaces are evil.
@@ -5111,5 +5130,5 @@
 
 	  _printrec_tiny(*ZONECACHE, 'n', \%recflags,
-		$dom, $host, $type, $val, $dist, $weight, $port, $ttl, $loc, $stamp)
+		$dom, $host, $type, $val, $dist, $weight, $port, $ttl, $loc, $stamp, $expires, $stampactive)
 		if *ZONECACHE;
 
@@ -5150,5 +5169,70 @@
 # Utility sub for __export_tiny above
 sub _printrec_tiny {
-  my ($datafile,$revrec,$recflags,$zone,$host,$type,$val,$dist,$weight,$port,$ttl,$loc,$stamp) = @_;
+  my ($datafile,$revrec,$recflags,$zone,$host,$type,$val,$dist,$weight,$port,$ttl,$loc,$stamp,$expires,$stampactive) = @_;
+
+  $loc = '' if !$loc;	# de-nullify - just in case
+##fixme:  handle case of record-with-location-that-doesn't-exist better.
+# note this currently fails safe (tested) - records with a location that
+# doesn't exist will not be sent to any client
+#	$loc = '' if !$lochash->{$loc};
+
+
+## Records that are valid only before or after a set time
+
+# record due to expire sometime is the complex case.  we don't want to just
+# rely on tinydns' auto-adjusting TTLs, because the default TTL in that case
+# is one day instead of the SOA minttl as BIND might do.
+
+# consider the case where a record is set to expire a week ahead, but the next
+# day later you want to change it NOW (or as NOWish as you get with your DNS
+# management practice).  but now you're stuck, because someone, somewhere, 
+# has just done a lookup before your latest change was published, and they'll
+# be caching that old, broken record for 1 day instead of your zone default
+# TTL.
+
+# $stamp-$ttl is the *latest* we can publish the record with the defined TTL
+# to still have the expiry happen as scheduled, but we need to find some
+# *earlier* point.  We can maybe guess, and 2x TTL is probably reasonable,
+# but we need info on the export frequency.
+
+# export the normal, non-expiring record up until $stamp-<guesstimate>, then
+# switch to exporting a record with the TAI64 stamp and a 0 TTL so tinydns
+# takes over TTL management.
+
+  if ($stampactive) {
+    if ($expires) {
+      # record expires at $stamp;  decide if we need to keep the TTL and ignore
+      # the stamp for a time or if we need to change the TTL to 0 and convert
+      # $stamp to TAI64 so tinydns can use $stamp to autoadjust the TTL on the fly.
+# extra hack, optimally needs more knowledge of data export frequency
+# smack the idiot customer who insists on 0 TTLs;  they can suck up and
+# deal with a 10-minute TTL.  especially on scheduled changes.  note this
+# should be (export freq * 2), but we don't know the actual export frequency.
+$ttl = 300 if $ttl == 0;	#hack phtui
+      my $ahead = (86400 < $ttl*2 ? 86400 : $ttl*2);
+      if ((time() + $ahead) < $stamp) {
+        # more than 2x TTL OR more than one day (whichever is less) from expiry time;  publish normal record
+        $stamp = '';
+      } else {
+        # less than 2x TTL from expiry time, let tinydns take over TTL management and publish the TAI64 stamp.
+        $ttl = 0;
+        $stamp = unixtai64($stamp);
+        $stamp =~ s/\@//;
+      }
+    } else {
+      # record is "active after";  convert epoch from database to TAI64, publish, and collect $200.
+      $stamp = unixtai64($stamp);
+      $stamp =~ s/\@//;
+    }
+  } else {
+    # flag for active timestamp is false;  don't actually put a timestamp in the output
+    $stamp = '';
+  }
+
+  # support tinydns' auto-TTL
+  $ttl = '' if $ttl == -1;
+# these are WAY FREAKING HIGH - higher even than most TLD registry TTLs!
+# NS          259200  => 3d
+# all others   86400  => 1d
 
   if ($revrec eq 'y') {
