backfill_pid_title.php 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. <?php
  2. // tools/backfill_pid_title.php
  3. // Backfill PID + land title id for rows that already have lat/lng.
  4. // Works in CLI and via browser (query params).
  5. declare(strict_types=1);
  6. // ---------- runtime mode ----------
  7. $isCli = (PHP_SAPI === 'cli');
  8. // Define STDOUT/STDERR when running via web
  9. if (!$isCli) {
  10. if (!defined('STDOUT')) define('STDOUT', fopen('php://output', 'w'));
  11. if (!defined('STDERR')) define('STDERR', fopen('php://output', 'w'));
  12. @header('Content-Type: text/plain; charset=utf-8');
  13. @set_time_limit(0);
  14. }
  15. // ---------- config / env ----------
  16. $DB_HOST = getenv('MYSQL_HOST') ?: 'db';
  17. $DB_NAME = getenv('MYSQL_DATABASE') ?: 'councils';
  18. $DB_USER = getenv('MYSQL_USER') ?: 'root';
  19. $DB_PASS = getenv('MYSQL_PASSWORD') ?: getenv('MYSQL_ROOT_PASSWORD');
  20. $DB_PORT = (int)(getenv('MYSQL_PORT') ?: 3306);
  21. // Where to call list_lookup.php
  22. $LOOKUP_URL = getenv('LOOKUP_URL') ?: 'https://modulosdesign.com.au/internal/classes/list_lookup.php';
  23. // Throttle between requests (microseconds)
  24. $THROTTLE_US = (int)(getenv('LOOKUP_THROTTLE_US') ?: 150000); // 150 ms
  25. // ---------- args ----------
  26. if ($isCli) {
  27. $opt = getopt('', ['table::','all','limit::','dry-run','lookup::']);
  28. } else {
  29. $opt = [
  30. 'table' => $_GET['table'] ?? null,
  31. 'all' => isset($_GET['all']) ? '1' : null,
  32. 'limit' => $_GET['limit'] ?? null,
  33. 'dry-run' => isset($_GET['dry_run']) ? '1' : null,
  34. 'lookup' => $_GET['lookup'] ?? null,
  35. ];
  36. }
  37. if (!empty($opt['lookup'])) $LOOKUP_URL = (string)$opt['lookup'];
  38. $tableFilter = $opt['table'] ?? null;
  39. $doAll = isset($opt['all']) || !$tableFilter;
  40. $limit = isset($opt['limit']) ? max(1,(int)$opt['limit']) : 500;
  41. $dryRun = isset($opt['dry-run']);
  42. // ---------- db connect ----------
  43. $dsn = "mysql:host={$DB_HOST};port={$DB_PORT};dbname={$DB_NAME};charset=utf8mb4";
  44. $pdo = new PDO($dsn, $DB_USER, $DB_PASS, [
  45. PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
  46. PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
  47. ]);
  48. // ---------- helpers ----------
  49. function info($s){ fwrite(STDOUT, $s . PHP_EOL); }
  50. function warnln($s){ fwrite(STDERR, "[WARN] $s" . PHP_EOL); }
  51. function table_has_cols(PDO $pdo, string $t, array $cols): array {
  52. $have = [];
  53. $st = $pdo->query("SHOW COLUMNS FROM `{$t}`");
  54. while ($r = $st->fetch()) $have[strtolower($r['Field'])] = true;
  55. $out = [];
  56. foreach ($cols as $c) $out[$c] = isset($have[strtolower($c)]);
  57. return $out;
  58. }
  59. function ensure_cols(PDO $pdo, string $t){
  60. // Use your live schema names: property_id + title_reference (+ optional areas)
  61. $need = table_has_cols($pdo, $t, ['property_id','title_reference','area_sqm','area_ha']);
  62. $sqls = [];
  63. if (!$need['property_id']) $sqls[] = "ALTER TABLE `{$t}` ADD COLUMN property_id VARCHAR(32) NULL";
  64. if (!$need['title_reference']) $sqls[] = "ALTER TABLE `{$t}` ADD COLUMN title_reference VARCHAR(64) NULL";
  65. if (!$need['area_sqm']) $sqls[] = "ALTER TABLE `{$t}` ADD COLUMN area_sqm DOUBLE NULL";
  66. if (!$need['area_ha']) $sqls[] = "ALTER TABLE `{$t}` ADD COLUMN area_ha DOUBLE NULL";
  67. foreach ($sqls as $sql) { $pdo->exec($sql); }
  68. }
  69. function post_json(string $url, array $payload, int $timeout=15): array {
  70. $ch = curl_init($url);
  71. $body = json_encode($payload, JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE);
  72. curl_setopt_array($ch, [
  73. CURLOPT_POST => true,
  74. CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
  75. CURLOPT_POSTFIELDS => $body,
  76. CURLOPT_RETURNTRANSFER => true,
  77. CURLOPT_CONNECTTIMEOUT => $timeout,
  78. CURLOPT_TIMEOUT => $timeout,
  79. CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
  80. ]);
  81. $resp = curl_exec($ch);
  82. if ($resp === false) {
  83. $err = curl_error($ch); curl_close($ch);
  84. throw new RuntimeException("cURL error: $err");
  85. }
  86. $code = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
  87. curl_close($ch);
  88. if ($code < 200 || $code >= 300) throw new RuntimeException("HTTP $code from lookup");
  89. $j = json_decode($resp, true);
  90. if (!is_array($j)) throw new RuntimeException("Bad JSON from lookup");
  91. return $j;
  92. }
  93. // ---------- pick tables ----------
  94. $tables = [];
  95. if ($doAll) {
  96. $st = $pdo->query("SHOW TABLES");
  97. while ($r = $st->fetch(PDO::FETCH_NUM)) {
  98. if (strpos($r[0], 'da_') === 0) $tables[] = $r[0];
  99. }
  100. } else {
  101. $tables = [$tableFilter];
  102. }
  103. if (!$tables) { info("No tables."); exit(0); }
  104. // ---------- process ----------
  105. foreach ($tables as $t) {
  106. info("Table: {$t}");
  107. // Needs lat/lng
  108. $has = table_has_cols($pdo, $t, ['lat','lng']);
  109. if (!$has['lat'] || !$has['lng']) { info(" skip (no lat/lng)"); continue; }
  110. ensure_cols($pdo, $t);
  111. // Rows that need fill (lat/lng present; missing property_id or title_reference)
  112. $sql = "
  113. SELECT id, lat, lng, property_id, title_reference
  114. FROM `{$t}`
  115. WHERE lat IS NOT NULL AND lng IS NOT NULL
  116. AND (title_reference IS NULL OR title_reference = '')
  117. ORDER BY id ASC
  118. LIMIT {$limit}";
  119. $todo = $pdo->query($sql)->fetchAll();
  120. if (!$todo) { info(" nothing to do"); continue; }
  121. info(" found ".count($todo)." row(s) to backfill (limit {$limit})");
  122. $upd = $pdo->prepare("
  123. UPDATE `{$t}`
  124. SET property_id = ?, title_reference = ?, area_sqm = ?, area_ha = ?
  125. WHERE id = ?");
  126. foreach ($todo as $r) {
  127. $id = (int)$r['id'];
  128. $lat = (float)$r['lat'];
  129. $lng = (float)$r['lng'];
  130. try {
  131. $resp = post_json($LOOKUP_URL, ['lat'=>$lat,'lng'=>$lng]);
  132. if (empty($resp['ok'])) throw new RuntimeException($resp['error'] ?? 'lookup failed');
  133. $pid = isset($resp['pid']) ? preg_replace('/[^0-9]/','', (string)$resp['pid']) : null;
  134. $titleId = $resp['title_id'] ?? null;
  135. $areaSqm = isset($resp['area_sqm']) ? (float)$resp['area_sqm'] : null;
  136. $areaHa = isset($resp['area_ha']) ? (float)$resp['area_ha'] : null;
  137. if ($dryRun) {
  138. info(sprintf(" id=%d lat=%.6f lng=%.6f -> pid=%s title=%s area=%.1f sqm",
  139. $id, $lat, $lng, ($pid?:'NULL'), ($titleId?:'NULL'), ($areaSqm?:0)));
  140. } else {
  141. $upd->execute([$pid, $titleId, $areaSqm, $areaHa, $id]);
  142. }
  143. } catch (Throwable $e) {
  144. warnln(" id={$id} error: ".$e->getMessage());
  145. }
  146. if ($THROTTLE_US > 0) usleep($THROTTLE_US);
  147. }
  148. info(" done {$t}");
  149. }
  150. info("All done.");