Du kannst nicht mehr als 25 Themen auswählen Themen müssen entweder mit einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.

355 Zeilen
13KB

  1. <#
  2. .SYNOPSIS
  3. Migrates territory data from database\myAccessFile.accdb to database\app.sqlite.
  4. .DESCRIPTION
  5. Reads the Territories, Households, and HouseholderNames tables from the Access
  6. database and inserts them into the matching SQLite tables.
  7. Prerequisites:
  8. - 64-bit Microsoft Access Database Engine (ACE OLEDB 12.0):
  9. https://www.microsoft.com/en-us/download/details.aspx?id=54920
  10. If you have 32-bit Office installed, run from 32-bit PowerShell instead:
  11. %SystemRoot%\SysWOW64\WindowsPowerShell\v1.0\powershell.exe
  12. - sqlite3.exe on PATH, in the project root, or in database\ .
  13. Download: https://www.sqlite.org/download.html (sqlite-tools-win-x64-*.zip)
  14. The script will attempt to download it automatically if not found.
  15. .PARAMETER AccessFile
  16. Path to the .accdb file. Relative paths resolve from the project root.
  17. Default: database\myAccessFile.accdb
  18. .PARAMETER SQLiteFile
  19. Path to the SQLite database. Relative paths resolve from the project root.
  20. Default: database\app.sqlite
  21. .PARAMETER DryRun
  22. Print Access row counts without writing any data to SQLite.
  23. .PARAMETER Sqlite3Path
  24. Explicit path to sqlite3.exe (overrides automatic search).
  25. .EXAMPLE
  26. .\database\Migrate-AccessToSQLite.ps1
  27. .\database\Migrate-AccessToSQLite.ps1 -DryRun
  28. .\database\Migrate-AccessToSQLite.ps1 -Sqlite3Path C:\sqlite\sqlite3.exe
  29. #>
  30. [CmdletBinding()]
  31. param(
  32. [string]$AccessFile = "database\myAccessFile.accdb",
  33. [string]$SQLiteFile = "database\app.sqlite",
  34. [switch]$DryRun,
  35. [string]$Sqlite3Path = ""
  36. )
  37. Set-StrictMode -Version Latest
  38. $ErrorActionPreference = 'Stop'
  39. # ── Resolve project root ───────────────────────────────────────────────────────
  40. $projectRoot = if ($PSScriptRoot) { $PSScriptRoot } else { $PWD.Path }
  41. if ($projectRoot -match '[/\\]database$') {
  42. $projectRoot = Split-Path $projectRoot -Parent
  43. }
  44. function Resolve-ProjectPath([string]$p) {
  45. if ([System.IO.Path]::IsPathRooted($p)) { return $p }
  46. return Join-Path $projectRoot $p
  47. }
  48. $accessPath = Resolve-ProjectPath $AccessFile
  49. $sqlitePath = Resolve-ProjectPath $SQLiteFile
  50. if (-not (Test-Path $accessPath)) {
  51. Write-Error "Access file not found: $accessPath"
  52. exit 1
  53. }
  54. if (-not (Test-Path $sqlitePath)) {
  55. Write-Error "SQLite file not found: $sqlitePath`nRun the PHP migrations first to create the schema."
  56. exit 1
  57. }
  58. Write-Host "Access file : $accessPath"
  59. Write-Host "SQLite file : $sqlitePath"
  60. # ── Locate sqlite3.exe ─────────────────────────────────────────────────────────
  61. function Find-Sqlite3([string]$hint) {
  62. $candidates = @(
  63. $hint,
  64. (Get-Command sqlite3.exe -ErrorAction SilentlyContinue |
  65. Select-Object -ExpandProperty Source -ErrorAction SilentlyContinue),
  66. (Join-Path $projectRoot "sqlite3.exe"),
  67. (Join-Path $projectRoot "database\sqlite3.exe"),
  68. "C:\sqlite\sqlite3.exe",
  69. "C:\tools\sqlite3.exe",
  70. "C:\ProgramData\chocolatey\bin\sqlite3.exe"
  71. ) | Where-Object { $_ -and (Test-Path $_ -ErrorAction SilentlyContinue) }
  72. return $candidates | Select-Object -First 1
  73. }
  74. $sqlite3 = Find-Sqlite3 $Sqlite3Path
  75. if (-not $sqlite3 -and -not $DryRun) {
  76. Write-Host ""
  77. Write-Host "sqlite3.exe not found — attempting automatic download ..."
  78. $destExe = Join-Path $projectRoot "sqlite3.exe"
  79. try {
  80. $dlPage = Invoke-WebRequest -Uri "https://www.sqlite.org/download.html" -UseBasicParsing
  81. $zipName = ($dlPage.Content |
  82. Select-String -Pattern 'sqlite-tools-win-x64-[\d]+\.zip' -AllMatches
  83. ).Matches[0].Value
  84. if (-not $zipName) { throw "Could not parse download filename from sqlite.org" }
  85. $zipUrl = "https://www.sqlite.org/$zipName"
  86. $zipPath = Join-Path $env:TEMP $zipName
  87. Write-Host " Downloading $zipUrl ..."
  88. Invoke-WebRequest -Uri $zipUrl -OutFile $zipPath -UseBasicParsing
  89. $extractDir = Join-Path $env:TEMP "sqlite_extract_$(Get-Random)"
  90. Expand-Archive -Path $zipPath -DestinationPath $extractDir -Force
  91. $extracted = Get-ChildItem -Path $extractDir -Filter sqlite3.exe -Recurse |
  92. Select-Object -First 1
  93. if ($extracted) {
  94. Copy-Item $extracted.FullName $destExe -Force
  95. $sqlite3 = $destExe
  96. Write-Host " sqlite3.exe saved to: $destExe"
  97. } else {
  98. throw "sqlite3.exe not found inside downloaded archive"
  99. }
  100. } catch {
  101. Write-Host ""
  102. Write-Host " Automatic download failed: $_"
  103. Write-Host ""
  104. Write-Host " Please download sqlite3.exe manually:"
  105. Write-Host " 1. Visit https://www.sqlite.org/download.html"
  106. Write-Host " 2. Download 'sqlite-tools-win-x64-*.zip'"
  107. Write-Host " 3. Extract sqlite3.exe to: $projectRoot"
  108. Write-Host " Then re-run this script."
  109. exit 1
  110. }
  111. }
  112. if ($sqlite3) {
  113. Write-Host "sqlite3.exe : $sqlite3"
  114. }
  115. # ── Open Access database via OleDb ────────────────────────────────────────────
  116. Add-Type -AssemblyName System.Data
  117. $script:oleConn = [System.Data.OleDb.OleDbConnection]::new(
  118. "Provider=Microsoft.ACE.OLEDB.12.0;Data Source=$accessPath;Mode=Read;"
  119. )
  120. try {
  121. $script:oleConn.Open()
  122. } catch {
  123. Write-Error @"
  124. Cannot open Access database. Error: $($_.Exception.Message)
  125. Ensure the 64-bit Microsoft Access Database Engine is installed:
  126. https://www.microsoft.com/en-us/download/details.aspx?id=54920
  127. If you have 32-bit Office, run this script from 32-bit PowerShell:
  128. %SystemRoot%\SysWOW64\WindowsPowerShell\v1.0\powershell.exe .\database\Migrate-AccessToSQLite.ps1
  129. "@
  130. exit 1
  131. }
  132. # Reads all rows from an Access table and returns a List[hashtable].
  133. # Using a DataReader avoids DataTable/pipeline-unwrap quirks in PowerShell.
  134. function Read-AccessTable([string]$TableName) {
  135. $rows = [System.Collections.Generic.List[hashtable]]::new()
  136. $cmd = [System.Data.OleDb.OleDbCommand]::new("SELECT * FROM [$TableName]", $script:oleConn)
  137. $rdr = $cmd.ExecuteReader()
  138. $n = $rdr.FieldCount
  139. $cols = [string[]]::new($n)
  140. for ($i = 0; $i -lt $n; $i++) { $cols[$i] = $rdr.GetName($i) }
  141. while ($rdr.Read()) {
  142. $row = @{}
  143. for ($i = 0; $i -lt $n; $i++) {
  144. $v = $rdr.GetValue($i)
  145. $row[$cols[$i]] = if ($v -is [System.DBNull]) { $null } else { $v }
  146. }
  147. $rows.Add($row)
  148. }
  149. $rdr.Dispose()
  150. $cmd.Dispose()
  151. return $rows
  152. }
  153. # ── SQL value helpers ─────────────────────────────────────────────────────────
  154. function Esc-Str([object]$v) {
  155. if ($null -eq $v) { return 'NULL' }
  156. $s = [string]$v
  157. if ($s -eq '') { return 'NULL' }
  158. return "'" + $s.Replace("'", "''") + "'"
  159. }
  160. # For NOT NULL VARCHAR columns: fall back to empty string rather than NULL.
  161. function EscStrNN([object]$v) {
  162. if ($null -eq $v) { return "''" }
  163. return "'" + ([string]$v).Replace("'", "''") + "'"
  164. }
  165. function Esc-Int([object]$v) {
  166. if ($null -eq $v) { return 'NULL' }
  167. $i = 0
  168. if ([int]::TryParse([string]$v, [ref]$i)) { return [string]$i }
  169. return 'NULL'
  170. }
  171. function Esc-Float([object]$v) {
  172. if ($null -eq $v) { return 'NULL' }
  173. $f = 0.0
  174. $style = [System.Globalization.NumberStyles]::Float
  175. $culture = [System.Globalization.CultureInfo]::InvariantCulture
  176. if ([double]::TryParse([string]$v, $style, $culture, [ref]$f)) {
  177. return $f.ToString($culture)
  178. }
  179. return 'NULL'
  180. }
  181. function Esc-Bool([object]$v) {
  182. if ($null -eq $v) { return '0' }
  183. $s = ([string]$v).ToLower().Trim()
  184. if ($s -in @('true', '1', 'yes', '-1', 'on')) { return '1' }
  185. return '0'
  186. }
  187. function Esc-Date([object]$v, [string]$fmt = 'yyyy-MM-dd') {
  188. if ($null -eq $v) { return 'NULL' }
  189. if ($v -is [datetime]) { return "'" + ([datetime]$v).ToString($fmt) + "'" }
  190. $dt = [datetime]::MinValue
  191. if ([datetime]::TryParse([string]$v, [ref]$dt)) {
  192. return "'" + $dt.ToString($fmt) + "'"
  193. }
  194. return 'NULL'
  195. }
  196. function Esc-DateTime([object]$v) { return Esc-Date $v 'yyyy-MM-dd HH:mm:ss' }
  197. # ── Read all three tables ─────────────────────────────────────────────────────
  198. Write-Host ""
  199. Write-Host "[1/3] Reading Territories ..."
  200. $tTerritories = Read-AccessTable 'Territories'
  201. Write-Host " $($tTerritories.Count) row(s)"
  202. Write-Host "[2/3] Reading Households ..."
  203. $tHouseholds = Read-AccessTable 'Households'
  204. Write-Host " $($tHouseholds.Count) row(s)"
  205. Write-Host "[3/3] Reading HouseholderNames ..."
  206. $tHouseholderNames = Read-AccessTable 'HouseholderNames'
  207. Write-Host " $($tHouseholderNames.Count) row(s)"
  208. $script:oleConn.Close()
  209. if ($DryRun) {
  210. Write-Host ""
  211. Write-Host "-- DRY RUN: no data written --"
  212. Write-Host (" {0,-25} {1}" -f "territories", $tTerritories.Count)
  213. Write-Host (" {0,-25} {1}" -f "households", $tHouseholds.Count)
  214. Write-Host (" {0,-25} {1}" -f "householder_names", $tHouseholderNames.Count)
  215. exit 0
  216. }
  217. # ── Build SQL ─────────────────────────────────────────────────────────────────
  218. $now = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
  219. $sql = [System.Text.StringBuilder]::new()
  220. [void]$sql.AppendLine("PRAGMA foreign_keys = OFF;")
  221. [void]$sql.AppendLine("PRAGMA journal_mode = WAL;")
  222. [void]$sql.AppendLine("BEGIN TRANSACTION;")
  223. [void]$sql.AppendLine("")
  224. [void]$sql.AppendLine("DELETE FROM householder_names;")
  225. [void]$sql.AppendLine("DELETE FROM households;")
  226. [void]$sql.AppendLine("DELETE FROM territories;")
  227. [void]$sql.AppendLine("")
  228. # Territories
  229. foreach ($row in $tTerritories) {
  230. $id = [int]$row['Id']
  231. $name = EscStrNN $row['Name']
  232. $desc = Esc-Str $row['Description']
  233. $coord = Esc-Str $row['Coordinates']
  234. [void]$sql.AppendLine(
  235. "INSERT INTO territories (id, name, description, coordinates, created_at, updated_at) " +
  236. "VALUES ($id, $name, $desc, $coord, '$now', '$now');"
  237. )
  238. }
  239. [void]$sql.AppendLine("")
  240. # Households
  241. foreach ($row in $tHouseholds) {
  242. $id = [int]$row['Id']
  243. $terrId = [int]$row['TerritoryId']
  244. $addr = EscStrNN $row['Address']
  245. $sNum = Esc-Int $row['StreetNumber']
  246. $sName = Esc-Str $row['StreetName']
  247. $lat = Esc-Float $row['Latitude']
  248. $lon = Esc-Float $row['Longitude']
  249. $isBiz = Esc-Bool $row['IsBusiness']
  250. $dnc = Esc-Bool $row['DoNotCall']
  251. $dncDate = Esc-Date $row['DoNotCallDate']
  252. $dncNotes = Esc-Str $row['DoNotCallNotes']
  253. $dncPriv = Esc-Str $row['DoNotCallPrivateNotes']
  254. [void]$sql.AppendLine(
  255. "INSERT INTO households (id, territory_id, address, street_number, street_name, " +
  256. "latitude, longitude, is_business, do_not_call, do_not_call_date, " +
  257. "do_not_call_notes, do_not_call_private_notes, created_at, updated_at) " +
  258. "VALUES ($id, $terrId, $addr, $sNum, $sName, $lat, $lon, $isBiz, $dnc, " +
  259. "$dncDate, $dncNotes, $dncPriv, '$now', '$now');"
  260. )
  261. }
  262. [void]$sql.AppendLine("")
  263. # HouseholderNames
  264. foreach ($row in $tHouseholderNames) {
  265. $id = [int]$row['Id']
  266. $hhId = [int]$row['HouseholdId']
  267. $name = EscStrNN $row['Name']
  268. $letRet = Esc-Bool $row['LetterReturned']
  269. $retDate = Esc-DateTime $row['ReturnDate']
  270. $creAt = Esc-DateTime $row['Created']
  271. if ($creAt -eq 'NULL') { $creAt = "'$now'" }
  272. [void]$sql.AppendLine(
  273. "INSERT INTO householder_names (id, household_id, name, letter_returned, " +
  274. "return_date, created_at, updated_at) " +
  275. "VALUES ($id, $hhId, $name, $letRet, $retDate, $creAt, '$now');"
  276. )
  277. }
  278. [void]$sql.AppendLine("")
  279. [void]$sql.AppendLine("COMMIT;")
  280. [void]$sql.AppendLine("PRAGMA foreign_keys = ON;")
  281. # ── Execute SQL via sqlite3 stdin ─────────────────────────────────────────────
  282. Write-Host ""
  283. Write-Host "Writing to SQLite ..."
  284. $output = $sql.ToString() | & $sqlite3 $sqlitePath 2>&1
  285. if ($LASTEXITCODE -ne 0) {
  286. Write-Host "sqlite3 output:"
  287. $output | ForEach-Object { Write-Host " $_" }
  288. Write-Error "sqlite3 exited with code $LASTEXITCODE"
  289. exit 1
  290. }
  291. # ── Verify ────────────────────────────────────────────────────────────────────
  292. Write-Host ""
  293. Write-Host "Migration complete. SQLite row counts:"
  294. $countSql = @(
  295. "SELECT 'territories', COUNT(*) FROM territories;"
  296. "SELECT 'households', COUNT(*) FROM households;"
  297. "SELECT 'householder_names', COUNT(*) FROM householder_names;"
  298. ) -join "`n"
  299. $counts = ($countSql | & $sqlite3 $sqlitePath) 2>&1
  300. foreach ($line in $counts) {
  301. $parts = [string]$line -split '\|'
  302. Write-Host (" {0,-25} {1}" -f $parts[0].Trim(), $parts[1].Trim())
  303. }

Powered by TurnKey Linux.