store a timestamp of the last location update seen from a subscriber

Timestamps are stored in the HLR DB in the new 'last_lu_seen' column
of the 'subscriber' table, in UTC and in granularity of seconds.

At present, osmo-hlr only records these timestamps but otherwise
makes no use of them. Because the timestamps are stored in a
human-readable form, they may already provide value to external
processes which need this information. For example:

  sqlite> select imsi,last_lu_seen from subscriber;
  901990000000001|2018-12-04 14:17:12

I didn't bother adding additional tests because the code added
with this commit is already being exercised by several calls
to db_subscr_lu() in db_test.c.

This change requires a HLR DB schema update. Existing databases
won't be upgraded automatically. However, osmo-hlr will refuse
to operate with databases which are not upgraded.

Change-Id: Ibeb49d45aec18451a260a6654b8c51b8fc3bec50
Related: OS#2838
diff --git a/src/db.c b/src/db.c
index df52f9b..f7cbace 100644
--- a/src/db.c
+++ b/src/db.c
@@ -27,7 +27,8 @@
 #include "db.h"
 #include "db_bootstrap.h"
 
-#define CURRENT_SCHEMA_VERSION	0
+/* This constant is currently duplicated in sql/hlr.sql and must be kept in sync! */
+#define CURRENT_SCHEMA_VERSION	1
 
 #define SEL_COLUMNS \
 	"id," \
@@ -42,7 +43,8 @@
 	"nam_ps," \
 	"lmsi," \
 	"ms_purged_cs," \
-	"ms_purged_ps"
+	"ms_purged_ps," \
+	"last_lu_seen"
 
 static const char *stmt_sql[] = {
 	[DB_STMT_SEL_BY_IMSI] = "SELECT " SEL_COLUMNS " FROM subscriber WHERE imsi = ?",
@@ -73,6 +75,7 @@
 		"INSERT INTO auc_3g (subscriber_id, algo_id_3g, k, op, opc, ind_bitlen)"
 		" VALUES($subscriber_id, $algo_id_3g, $k, $op, $opc, $ind_bitlen)",
 	[DB_STMT_AUC_3G_DELETE] = "DELETE FROM auc_3g WHERE subscriber_id = $subscriber_id",
+	[DB_STMT_SET_LAST_LU_SEEN] = "UPDATE subscriber SET last_lu_seen = datetime($val, 'unixepoch') WHERE id = $subscriber_id",
 };
 
 static void sql3_error_log_cb(void *arg, int err_code, const char *msg)
@@ -252,6 +255,41 @@
 	return true;
 }
 
+static int
+db_upgrade_v1(struct db_context *dbc)
+{
+	sqlite3_stmt *stmt;
+	int rc;
+	const char *update_stmt_sql = "ALTER TABLE subscriber ADD COLUMN last_lu_seen TIMESTAMP default NULL";
+	const char *set_schema_version_sql = "PRAGMA user_version = 1";
+
+	rc = sqlite3_prepare_v2(dbc->db, update_stmt_sql, -1, &stmt, NULL);
+	if (rc != SQLITE_OK) {
+		LOGP(DDB, LOGL_ERROR, "Unable to prepare SQL statement '%s'\n", update_stmt_sql);
+		return rc;
+	}
+	rc = sqlite3_step(stmt);
+	db_remove_reset(stmt);
+	sqlite3_finalize(stmt);
+	if (rc != SQLITE_DONE) {
+		LOGP(DDB, LOGL_ERROR, "Unable to update HLR database schema to version %d\n", 1);
+		return rc;
+	}
+
+	rc = sqlite3_prepare_v2(dbc->db, set_schema_version_sql, -1, &stmt, NULL);
+	if (rc != SQLITE_OK) {
+		LOGP(DDB, LOGL_ERROR, "Unable to prepare SQL statement '%s'\n", set_schema_version_sql);
+		return rc;
+	}
+	rc = sqlite3_step(stmt);
+	if (rc != SQLITE_DONE)
+		LOGP(DDB, LOGL_ERROR, "Unable to update HLR database schema to version %d\n", 1);
+
+	db_remove_reset(stmt);
+	sqlite3_finalize(stmt);
+	return rc;
+}
+
 static int db_get_user_version(struct db_context *dbc)
 {
 	const char *user_version_sql = "PRAGMA user_version";
@@ -346,12 +384,28 @@
 			     rc, sqlite3_errmsg(dbc->db));
 			goto out_free;
 		}
+		version = CURRENT_SCHEMA_VERSION;
 	}
 
 	LOGP(DDB, LOGL_NOTICE, "Database '%s' has HLR DB schema version %d\n", dbc->fname, version);
 
 	if (version < CURRENT_SCHEMA_VERSION && allow_upgrade) {
-		/* Future version upgrades will happen here. */
+		switch (version) {
+		case 0:
+			rc = db_upgrade_v1(dbc);
+			if (rc != SQLITE_DONE) {
+				LOGP(DDB, LOGL_ERROR, "Failed to upgrade HLR DB schema to version 1: (rc=%d) %s\n",
+				     rc, sqlite3_errmsg(dbc->db));
+				goto out_free;
+			}
+			version = 1;
+			/* fall through */
+		/* case N: ... */
+		default:
+			break;
+		}
+		LOGP(DDB, LOGL_NOTICE, "Database '%s' has been upgraded to HLR DB schema version %d\n",
+		     dbc->fname, version);
 	}
 
 	if (version != CURRENT_SCHEMA_VERSION) {