Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions packages/gaussdb-connection-string/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ interface SSLConfig {
rejectUnauthorized?: boolean
}

export interface HostSpec {
host: string
port: number
}

export interface ConnectionOptions {
host: string | null
password?: string
Expand All @@ -28,6 +33,27 @@ export interface ConnectionOptions {
options?: string
keepalives?: number

// Multi-host configuration
hosts?: HostSpec[]

// Load balancing mode (corresponds to JDBC autoBalance)
// false: no load balancing (default)
// true/'roundrobin'/'balance': round-robin mode
// 'shuffle': random mode
// 'leastconn': least connection mode (phase 2)
// 'priority[n]': priority round-robin mode (phase 2)
loadBalanceHosts?: boolean | string

// Target server type for connections (corresponds to JDBC targetServerType)
// 'any': connect to any node (default)
// 'master': connect to master node only
// 'slave': connect to slave node only
// 'preferSlave': prefer slave node, fallback to master if no slave available
targetServerType?: 'any' | 'master' | 'slave' | 'preferSlave'

// Host status recheck interval in seconds
hostRecheckSeconds?: number

// We allow any other options to be passed through
[key: string]: unknown
}
Expand Down
95 changes: 95 additions & 0 deletions packages/gaussdb-connection-string/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,41 @@
//Copyright (c) 2025 happy-game
//MIT License

/**
* Parse multiple hosts from hostname string
* @param {string} hostnameString - Comma-separated host:port string
* @param {number} defaultPort - Default port to use if not specified
* @returns {Array<{host: string, port: number}>} Array of host specs
*/
function parseMultipleHosts(hostnameString, defaultPort) {
const hostTokens = (hostnameString || '')
.split(',')
.map((token) => token.trim())
.filter((token) => token.length > 0)

if (hostTokens.length === 0) return []

return hostTokens.map((token) => {
const colonIndex = token.indexOf(':')

// Check if there's a port specification (host:port)
if (colonIndex > 0) {
const host = token.substring(0, colonIndex)
const port = parseInt(token.substring(colonIndex + 1), 10)
return {
host: decodeURIComponent(host),
port: port || defaultPort,
}
}

// No port specified
return {
host: decodeURIComponent(token),
port: defaultPort,
}
})
}

//parses a connection string
function parse(str, options = {}) {
//unix socket
Expand All @@ -18,6 +53,43 @@ function parse(str, options = {}) {
const config = {}
let result
let dummyHost = false
let multiHostString = null
let extractedPort = null

// Extract multi-host format BEFORE URL encoding
// Match pattern: gaussdb://[user:pass@]host1[:port1],host2[:port2],.../database
const multiHostMatch = str.match(/^(gaussdb:\/\/(?:[^@]+@)?)([^/?]+)(\/.*)?$/)
if (multiHostMatch && multiHostMatch[2] && multiHostMatch[2].includes(',')) {
multiHostString = multiHostMatch[2]

// Determine the default port by checking if only the last host has a port
// e.g., "node1,node2,node3:5433" -> default port is 5433
// e.g., "node1:5432,node2:5433" -> default port is 5432 (standard)
const hostParts = multiHostString.split(',')
let hasMultiplePortSpecifications = false
let lastPartPort = null

for (let i = 0; i < hostParts.length; i++) {
const part = hostParts[i].trim()
const colonIndex = part.indexOf(':')
const hasPort = colonIndex > 0

if (hasPort && i < hostParts.length - 1) {
hasMultiplePortSpecifications = true
break
}
if (hasPort && i === hostParts.length - 1) {
lastPartPort = part.substring(colonIndex + 1)
}
}

// If only last part has port, use it as default; otherwise use 5432
extractedPort = !hasMultiplePortSpecifications && lastPartPort ? lastPartPort : '5432'

// Replace with placeholder host
str = multiHostMatch[1] + '__MULTI_HOST_PLACEHOLDER__' + (multiHostMatch[3] || '')
}

if (/ |%[^a-f0-9]|%[a-f0-9][^a-f0-9]/i.test(str)) {
// Ensure spaces are encoded as %20
str = encodeURI(str).replace(/%25(\d\d)/g, '%$1')
Expand Down Expand Up @@ -61,6 +133,28 @@ function parse(str, options = {}) {
const pathname = result.pathname.slice(1) || null
config.database = pathname ? decodeURI(pathname) : null

// Parse multiple hosts if we extracted multi-host string
if (multiHostString) {
const defaultPort = parseInt(extractedPort, 10)
const hosts = parseMultipleHosts(multiHostString, defaultPort)
if (hosts.length > 0) {
config.hosts = hosts
// Set first host as default for backward compatibility
config.host = hosts[0].host
config.port = hosts[0].port
} else if (config.host === '__MULTI_HOST_PLACEHOLDER__') {
config.host = ''
config.port = defaultPort
}
}

// Parse loadBalanceHosts parameter
if (config.loadBalanceHosts === 'true' || config.loadBalanceHosts === '1') {
config.loadBalanceHosts = true
} else if (config.loadBalanceHosts === 'false' || config.loadBalanceHosts === '0') {
config.loadBalanceHosts = false
}

if (config.ssl === 'true' || config.ssl === '1') {
config.ssl = true
}
Expand Down Expand Up @@ -205,5 +299,6 @@ function parseIntoClientConfig(str) {
module.exports = parse

parse.parse = parse
parse.parseMultipleHosts = parseMultipleHosts
parse.toClientConfig = toClientConfig
parse.parseIntoClientConfig = parseIntoClientConfig
62 changes: 62 additions & 0 deletions packages/gaussdb-connection-string/test/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -447,4 +447,66 @@ describe('parse', function () {
const subject = parse(connectionString)
subject.port?.should.equal('1234')
})

it('parses multiple hosts with per-host ports and fallback default', function () {
const subject = parse('gaussdb://node1:5433,node2,node3:5434/mydb')
subject.host?.should.equal('node1')
subject.port?.should.equal(5433)
subject.database?.should.equal('mydb')
subject.hosts?.should.deep.equal([
{ host: 'node1', port: 5433 },
{ host: 'node2', port: 5432 },
{ host: 'node3', port: 5434 },
])
})

it('parses multiple hosts and uses the last host port as default', function () {
const subject = parse('gaussdb://node1,node2,node3:5439/mydb')
subject.host?.should.equal('node1')
subject.port?.should.equal(5439)
subject.database?.should.equal('mydb')
subject.hosts?.should.deep.equal([
{ host: 'node1', port: 5439 },
{ host: 'node2', port: 5439 },
{ host: 'node3', port: 5439 },
])
})

it('handles empty multi-host list by falling back to default port', function () {
const subject = parse('gaussdb://,,/mydb')
subject.host?.should.equal('')
subject.port?.should.equal(5432)
subject.database?.should.equal('mydb')
})

it('converts loadBalanceHosts query parameter to boolean true', function () {
parse('gaussdb://localhost/mydb?loadBalanceHosts=true').loadBalanceHosts?.should.equal(true)
parse('gaussdb://localhost/mydb?loadBalanceHosts=1').loadBalanceHosts?.should.equal(true)
})

it('converts loadBalanceHosts query parameter to boolean false', function () {
parse('gaussdb://localhost/mydb?loadBalanceHosts=false').loadBalanceHosts?.should.equal(false)
parse('gaussdb://localhost/mydb?loadBalanceHosts=0').loadBalanceHosts?.should.equal(false)
})

it('supports multi-host URL without pathname suffix', function () {
const subject = parse('gaussdb://node1,node2')
subject.host?.should.equal('node1')
subject.port?.should.equal(5432)
;(subject.database === null).should.equal(true)
})

it('keeps query host when parsed multi-host list is empty', function () {
const subject = parse('gaussdb://,,/mydb?host=override-host')
subject.host?.should.equal('override-host')
subject.database?.should.equal('mydb')
})

it('parseMultipleHosts returns empty list for empty input', function () {
parse.parseMultipleHosts(undefined, 5432).should.deep.equal([])
})

it('parseMultipleHosts uses default port for invalid host port', function () {
parse.parseMultipleHosts('node1:not-a-port', 5432).should.deep.equal([{ host: 'node1', port: 5432 }])
})
})
Loading