mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-08-03 07:01:56 +00:00
harden postgres migration integer safety
Before, integer preflight accepted values through JavaScript Number and the migration could truncate a target without an explicit confirmation. After, integer bounds and bigint strings are checked without precision loss, and destructive writes require ALLOW_DESTRUCTIVE_TARGET=true.
This commit is contained in:
parent
07d5450dc7
commit
8e6a1d57d7
2 changed files with 42 additions and 14 deletions
|
|
@ -8,6 +8,13 @@ const DATABASE_URL = process.env.DATABASE_URL
|
|||
const PG_SCHEMA = process.env.PG_SCHEMA || 'public'
|
||||
const BATCH_SIZE = Number(process.env.MIGRATION_BATCH_SIZE || 500)
|
||||
const DRY_RUN = String(process.env.DRY_RUN || 'false').toLowerCase() === 'true'
|
||||
const ALLOW_DESTRUCTIVE_TARGET = String(process.env.ALLOW_DESTRUCTIVE_TARGET || 'false').toLowerCase() === 'true'
|
||||
|
||||
const integerBounds = {
|
||||
smallint: { min: -32768n, max: 32767n },
|
||||
integer: { min: -2147483648n, max: 2147483647n },
|
||||
bigint: { min: -9223372036854775808n, max: 9223372036854775807n }
|
||||
}
|
||||
|
||||
const preferredOrder = [
|
||||
'migrationsMeta',
|
||||
|
|
@ -117,22 +124,34 @@ async function findOverlongVarcharValues(sqliteDb, tablesToMigrate, pgColumnsByT
|
|||
return issues
|
||||
}
|
||||
|
||||
function isIntegerCompatible(value) {
|
||||
if (value === null || value === undefined) return true
|
||||
|
||||
function parseIntegerValue(value) {
|
||||
if (typeof value === 'number') {
|
||||
return Number.isFinite(value) && Number.isInteger(value)
|
||||
if (!Number.isSafeInteger(value)) return null
|
||||
return BigInt(value)
|
||||
}
|
||||
|
||||
if (typeof value === 'string') {
|
||||
const trimmed = value.trim()
|
||||
if (!trimmed) return false
|
||||
if (!/^-?\d+$/.test(trimmed)) return false
|
||||
const parsed = Number(trimmed)
|
||||
return Number.isFinite(parsed)
|
||||
if (!trimmed || !/^-?\d+$/.test(trimmed)) return null
|
||||
try {
|
||||
return BigInt(trimmed)
|
||||
} catch (error) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
return null
|
||||
}
|
||||
|
||||
function isIntegerCompatible(value, dataType = 'bigint') {
|
||||
if (value === null || value === undefined) return true
|
||||
|
||||
const parsed = parseIntegerValue(value)
|
||||
if (parsed === null) return false
|
||||
|
||||
const bounds = integerBounds[dataType]
|
||||
if (!bounds) return false
|
||||
return parsed >= bounds.min && parsed <= bounds.max
|
||||
}
|
||||
|
||||
async function findIntegerTypeIssues(sqliteDb, tablesToMigrate, pgColumnsByTable) {
|
||||
|
|
@ -161,7 +180,7 @@ async function findIntegerTypeIssues(sqliteDb, tablesToMigrate, pgColumnsByTable
|
|||
let badCount = 0
|
||||
let sampleValue = null
|
||||
for (const row of rows) {
|
||||
if (!isIntegerCompatible(row.value)) {
|
||||
if (!isIntegerCompatible(row.value, dataType)) {
|
||||
badCount += 1
|
||||
if (sampleValue === null) sampleValue = row.value
|
||||
}
|
||||
|
|
@ -231,10 +250,12 @@ function convertValue(value, pgColumn) {
|
|||
}
|
||||
|
||||
if ((dataType === 'smallint' || dataType === 'integer' || dataType === 'bigint') && value !== null && value !== undefined) {
|
||||
if (typeof value === 'number') return value
|
||||
if (typeof value === 'string' && /^-?\d+$/.test(value.trim())) {
|
||||
return Number(value)
|
||||
if (!isIntegerCompatible(value, dataType)) return value
|
||||
if (dataType === 'bigint' && typeof value === 'string') {
|
||||
return value.trim()
|
||||
}
|
||||
if (typeof value === 'number') return value
|
||||
return Number(value)
|
||||
}
|
||||
|
||||
return value
|
||||
|
|
@ -353,6 +374,10 @@ async function main() {
|
|||
}
|
||||
|
||||
if (!DRY_RUN) {
|
||||
if (!ALLOW_DESTRUCTIVE_TARGET) {
|
||||
throw new Error('Migration writes are destructive. Set ALLOW_DESTRUCTIVE_TARGET=true after confirming the target database can be truncated.')
|
||||
}
|
||||
|
||||
await pg.query('BEGIN')
|
||||
await pg.query('SET session_replication_role = replica')
|
||||
|
||||
|
|
|
|||
|
|
@ -128,11 +128,14 @@ describe('migrateSqliteToPostgres script helpers', () => {
|
|||
expect(isIntegerCompatible('10')).to.equal(true)
|
||||
expect(isIntegerCompatible(10.5)).to.equal(false)
|
||||
expect(isIntegerCompatible('10.5')).to.equal(false)
|
||||
expect(isIntegerCompatible('32768', 'smallint')).to.equal(false)
|
||||
expect(isIntegerCompatible('9007199254740993', 'bigint')).to.equal(true)
|
||||
})
|
||||
|
||||
it('should coerce integer-like strings for postgres integer columns', () => {
|
||||
expect(convertValue('42', { data_type: 'integer' })).to.equal(42)
|
||||
expect(convertValue('-7', { data_type: 'bigint' })).to.equal(-7)
|
||||
expect(convertValue('-7', { data_type: 'bigint' })).to.equal('-7')
|
||||
expect(convertValue('9007199254740993', { data_type: 'bigint' })).to.equal('9007199254740993')
|
||||
expect(convertValue('4.2', { data_type: 'integer' })).to.equal('4.2')
|
||||
})
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue