@@ -1069,6 +1069,130 @@ function isDockerRunning(): boolean {
10691069 }
10701070}
10711071
1072+ /**
1073+ * Get PostgreSQL major version from a running container
1074+ * @returns Major version number (e.g., 15, 17, 18) or null if container not running
1075+ */
1076+ function getRunningPostgresVersion ( containerName : string ) : number | null {
1077+ try {
1078+ const result = spawnSync (
1079+ "docker" ,
1080+ [ "exec" , containerName , "psql" , "-U" , "postgres" , "-t" , "-c" , "SHOW server_version_num" ] ,
1081+ { stdio : "pipe" , encoding : "utf8" }
1082+ ) ;
1083+ if ( result . status === 0 && result . stdout ) {
1084+ const versionNum = parseInt ( result . stdout . trim ( ) , 10 ) ;
1085+ if ( ! isNaN ( versionNum ) ) {
1086+ return Math . floor ( versionNum / 10000 ) ; // e.g., 150000 -> 15
1087+ }
1088+ }
1089+ return null ;
1090+ } catch {
1091+ return null ;
1092+ }
1093+ }
1094+
1095+ /**
1096+ * Get target PostgreSQL major version from docker-compose.yml
1097+ * @returns Major version number or null if not found
1098+ */
1099+ function getTargetPostgresVersion ( composeFilePath : string ) : number | null {
1100+ try {
1101+ const content = fs . readFileSync ( composeFilePath , "utf8" ) ;
1102+ // Match postgres:XX image tag for sink-postgres service
1103+ const match = content . match ( / s i n k - p o s t g r e s : [ \s \S ] * ?i m a g e : \s * p o s t g r e s : ( \d + ) / ) ;
1104+ if ( match && match [ 1 ] ) {
1105+ return parseInt ( match [ 1 ] , 10 ) ;
1106+ }
1107+ return null ;
1108+ } catch {
1109+ return null ;
1110+ }
1111+ }
1112+
1113+ /**
1114+ * Run pg_upgrade to migrate PostgreSQL data between major versions
1115+ * Uses the new postgres image with old binaries installed
1116+ */
1117+ async function runPgUpgrade (
1118+ oldVersion : number ,
1119+ newVersion : number ,
1120+ projectDir : string
1121+ ) : Promise < boolean > {
1122+ console . log ( `\nMigrating PostgreSQL data from version ${ oldVersion } to ${ newVersion } ...` ) ;
1123+
1124+ const volumeName = "postgres_ai_sink_postgres_data" ;
1125+ const containerName = "postgres-ai-pg-upgrade" ;
1126+
1127+ // Build the upgrade script that runs inside the container
1128+ const upgradeScript = `
1129+ set -e
1130+
1131+ echo "Installing PostgreSQL ${ oldVersion } binaries..."
1132+ apt-get update -qq
1133+ apt-get install -y -qq postgresql-${ oldVersion } >/dev/null 2>&1
1134+
1135+ echo "Preparing data directories..."
1136+ mkdir -p /var/lib/postgresql/${ newVersion } /data
1137+ chown postgres:postgres /var/lib/postgresql/${ newVersion } /data
1138+ chmod 700 /var/lib/postgresql/${ newVersion } /data
1139+
1140+ # Initialize new data directory
1141+ echo "Initializing new PostgreSQL ${ newVersion } cluster..."
1142+ su postgres -c "/usr/lib/postgresql/${ newVersion } /bin/initdb -D /var/lib/postgresql/${ newVersion } /data"
1143+
1144+ # Run pg_upgrade
1145+ echo "Running pg_upgrade..."
1146+ cd /var/lib/postgresql
1147+ su postgres -c "/usr/lib/postgresql/${ newVersion } /bin/pg_upgrade \\
1148+ --old-datadir=/var/lib/postgresql/data \\
1149+ --new-datadir=/var/lib/postgresql/${ newVersion } /data \\
1150+ --old-bindir=/usr/lib/postgresql/${ oldVersion } /bin \\
1151+ --new-bindir=/usr/lib/postgresql/${ newVersion } /bin \\
1152+ --link"
1153+
1154+ # Replace old data with upgraded data
1155+ echo "Finalizing upgrade..."
1156+ rm -rf /var/lib/postgresql/data.old 2>/dev/null || true
1157+ mv /var/lib/postgresql/data /var/lib/postgresql/data.old
1158+ mv /var/lib/postgresql/${ newVersion } /data /var/lib/postgresql/data
1159+
1160+ echo "PostgreSQL upgrade completed successfully!"
1161+ ` ;
1162+
1163+ try {
1164+ // Remove any existing upgrade container
1165+ spawnSync ( "docker" , [ "rm" , "-f" , containerName ] , { stdio : "ignore" } ) ;
1166+
1167+ // Run upgrade in a temporary container
1168+ console . log ( "Starting upgrade container..." ) ;
1169+ const result = spawnSync (
1170+ "docker" ,
1171+ [
1172+ "run" ,
1173+ "--rm" ,
1174+ "--name" , containerName ,
1175+ "-v" , `${ volumeName } :/var/lib/postgresql/data` ,
1176+ `postgres:${ newVersion } ` ,
1177+ "bash" , "-c" , upgradeScript
1178+ ] ,
1179+ { stdio : "inherit" }
1180+ ) ;
1181+
1182+ if ( result . status === 0 ) {
1183+ console . log ( "✓ PostgreSQL upgrade completed successfully\n" ) ;
1184+ return true ;
1185+ } else {
1186+ console . error ( "✗ PostgreSQL upgrade failed" ) ;
1187+ return false ;
1188+ }
1189+ } catch ( error ) {
1190+ const message = error instanceof Error ? error . message : String ( error ) ;
1191+ console . error ( `PostgreSQL upgrade failed: ${ message } ` ) ;
1192+ return false ;
1193+ }
1194+ }
1195+
10721196/**
10731197 * Get docker compose command
10741198 */
@@ -1660,43 +1784,103 @@ mon
16601784 } ) ;
16611785mon
16621786 . command ( "update" )
1663- . description ( "update monitoring stack" )
1787+ . description ( "update monitoring stack (handles PostgreSQL major version upgrades automatically) " )
16641788 . action ( async ( ) => {
16651789 console . log ( "Updating PostgresAI monitoring stack...\n" ) ;
16661790
1791+ let composeFile : string ;
1792+ let projectDir : string ;
1793+
16671794 try {
1795+ // Get project directory
1796+ try {
1797+ ( { composeFile, projectDir } = await resolveOrInitPaths ( ) ) ;
1798+ } catch ( error ) {
1799+ const message = error instanceof Error ? error . message : String ( error ) ;
1800+ console . error ( message ) ;
1801+ process . exitCode = 1 ;
1802+ return ;
1803+ }
1804+
16681805 // Check if we're in a git repo
1669- const gitDir = path . resolve ( process . cwd ( ) , ".git" ) ;
1806+ const gitDir = path . resolve ( projectDir , ".git" ) ;
16701807 if ( ! fs . existsSync ( gitDir ) ) {
16711808 console . error ( "Not a git repository. Cannot update." ) ;
16721809 process . exitCode = 1 ;
16731810 return ;
16741811 }
16751812
1813+ // Get current PostgreSQL version from running container (before git pull)
1814+ const currentPgVersion = getRunningPostgresVersion ( "sink-postgres" ) ;
1815+ if ( currentPgVersion ) {
1816+ console . log ( `Current PostgreSQL version: ${ currentPgVersion } ` ) ;
1817+ }
1818+
16761819 // Fetch latest changes
1677- console . log ( "Fetching latest changes..." ) ;
1678- await execPromise ( " git fetch origin" ) ;
1820+ console . log ( "\nFetching latest changes..." ) ;
1821+ await execPromise ( ` git -C " ${ projectDir } " fetch origin` ) ;
16791822
16801823 // Check current branch
1681- const { stdout : branch } = await execPromise ( " git rev-parse --abbrev-ref HEAD" ) ;
1824+ const { stdout : branch } = await execPromise ( ` git -C " ${ projectDir } " rev-parse --abbrev-ref HEAD` ) ;
16821825 const currentBranch = branch . trim ( ) ;
16831826 console . log ( `Current branch: ${ currentBranch } ` ) ;
16841827
16851828 // Pull latest changes
16861829 console . log ( "Pulling latest changes..." ) ;
1687- const { stdout : pullOut } = await execPromise ( " git pull origin " + currentBranch ) ;
1830+ const { stdout : pullOut } = await execPromise ( ` git -C " ${ projectDir } " pull origin ${ currentBranch } ` ) ;
16881831 console . log ( pullOut ) ;
16891832
1833+ // Get target PostgreSQL version from updated docker-compose.yml
1834+ const targetPgVersion = getTargetPostgresVersion ( composeFile ) ;
1835+ if ( targetPgVersion ) {
1836+ console . log ( `Target PostgreSQL version: ${ targetPgVersion } ` ) ;
1837+ }
1838+
1839+ // Check if PostgreSQL major version upgrade is needed
1840+ const needsPgUpgrade = currentPgVersion && targetPgVersion && currentPgVersion !== targetPgVersion ;
1841+
1842+ if ( needsPgUpgrade ) {
1843+ console . log ( `\n⚠ PostgreSQL major version change detected: ${ currentPgVersion } → ${ targetPgVersion } ` ) ;
1844+
1845+ // Stop services before upgrade
1846+ console . log ( "\nStopping services for PostgreSQL upgrade..." ) ;
1847+ await runCompose ( [ "stop" ] ) ;
1848+
1849+ // Run pg_upgrade
1850+ const upgradeSuccess = await runPgUpgrade ( currentPgVersion , targetPgVersion , projectDir ) ;
1851+
1852+ if ( ! upgradeSuccess ) {
1853+ console . error ( "\n✗ PostgreSQL upgrade failed" ) ;
1854+ console . error ( "Your data is preserved in the original format." ) ;
1855+ console . error ( "You can either:" ) ;
1856+ console . error ( " 1. Fix the issue and run 'postgres-ai mon update' again" ) ;
1857+ console . error ( " 2. Reset the database: postgres-ai mon reset sink-postgres" ) ;
1858+ process . exitCode = 1 ;
1859+ return ;
1860+ }
1861+ }
1862+
16901863 // Update Docker images
1691- console . log ( "\nUpdating Docker images..." ) ;
1864+ console . log ( "Updating Docker images..." ) ;
16921865 const code = await runCompose ( [ "pull" ] ) ;
16931866
1694- if ( code === 0 ) {
1867+ if ( code !== 0 ) {
1868+ console . error ( "\n✗ Docker image update failed" ) ;
1869+ process . exitCode = 1 ;
1870+ return ;
1871+ }
1872+
1873+ // Restart services to apply updates
1874+ console . log ( "\nRestarting services..." ) ;
1875+ const restartCode = await runCompose ( [ "up" , "-d" , "--force-recreate" ] ) ;
1876+
1877+ if ( restartCode === 0 ) {
16951878 console . log ( "\n✓ Update completed successfully" ) ;
1696- console . log ( "\nTo apply updates, restart monitoring services:" ) ;
1697- console . log ( " postgres-ai mon restart" ) ;
1879+ if ( needsPgUpgrade ) {
1880+ console . log ( `✓ PostgreSQL upgraded from ${ currentPgVersion } to ${ targetPgVersion } ` ) ;
1881+ }
16981882 } else {
1699- console . error ( "\n✗ Docker image update failed " ) ;
1883+ console . error ( "\n✗ Failed to restart services " ) ;
17001884 process . exitCode = 1 ;
17011885 }
17021886 } catch ( error ) {
0 commit comments