-
Notifications
You must be signed in to change notification settings - Fork 184
Add search_path support in TOML config for PostgreSQL #250
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -349,6 +349,22 @@ function validateSourceConfig(source: SourceConfig, configPath: string): void { | |||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // Validate search_path (PostgreSQL only) | ||||||||||||||||||||||||
| if (source.search_path !== undefined) { | ||||||||||||||||||||||||
| if (source.type !== "postgres") { | ||||||||||||||||||||||||
| throw new Error( | ||||||||||||||||||||||||
| `Configuration file ${configPath}: source '${source.id}' has 'search_path' but it is only supported for PostgreSQL sources.` | ||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| if (typeof source.search_path !== "string" || source.search_path.trim().length === 0) { | ||||||||||||||||||||||||
| throw new Error( | ||||||||||||||||||||||||
| `Configuration file ${configPath}: source '${source.id}' has invalid search_path. ` + | ||||||||||||||||||||||||
| `Must be a non-empty string of comma-separated schema names (e.g., "myschema,public").` | ||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
tianzhou marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
|
||||||||||||||||||||||||
| const searchPathSchemas = source.search_path | |
| .split(",") | |
| .map((schema) => schema.trim()) | |
| .filter((schema) => schema.length > 0); | |
| if (searchPathSchemas.length === 0) { | |
| throw new Error( | |
| `Configuration file ${configPath}: source '${source.id}' has invalid search_path. ` + | |
| `Must contain at least one non-empty schema name (e.g., "myschema,public").` | |
| ); | |
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -15,6 +15,7 @@ import { | |||||||||||||||||||||||||||||||||||||||
| import { SafeURL } from "../../utils/safe-url.js"; | ||||||||||||||||||||||||||||||||||||||||
| import { obfuscateDSNPassword } from "../../utils/dsn-obfuscate.js"; | ||||||||||||||||||||||||||||||||||||||||
| import { SQLRowLimiter } from "../../utils/sql-row-limiter.js"; | ||||||||||||||||||||||||||||||||||||||||
| import { quoteIdentifier } from "../../utils/identifier-quoter.js"; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||
| * PostgreSQL DSN Parser | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -110,6 +111,9 @@ export class PostgresConnector implements Connector { | |||||||||||||||||||||||||||||||||||||||
| // Source ID is set by ConnectorManager after cloning | ||||||||||||||||||||||||||||||||||||||||
| private sourceId: string = "default"; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| // Default schema for discovery methods (first entry from search_path, or "public") | ||||||||||||||||||||||||||||||||||||||||
| private defaultSchema: string = "public"; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| getId(): string { | ||||||||||||||||||||||||||||||||||||||||
| return this.sourceId; | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -119,6 +123,9 @@ export class PostgresConnector implements Connector { | |||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| async connect(dsn: string, initScript?: string, config?: ConnectorConfig): Promise<void> { | ||||||||||||||||||||||||||||||||||||||||
| // Reset default schema in case this connector instance is re-used across connect() calls | ||||||||||||||||||||||||||||||||||||||||
| this.defaultSchema = "public"; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||
| const poolConfig = await this.dsnParser.parse(dsn, config); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
|
|
@@ -127,6 +134,18 @@ export class PostgresConnector implements Connector { | |||||||||||||||||||||||||||||||||||||||
| poolConfig.options = (poolConfig.options || '') + ' -c default_transaction_read_only=on'; | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| // Set search_path if configured | ||||||||||||||||||||||||||||||||||||||||
| if (config?.searchPath) { | ||||||||||||||||||||||||||||||||||||||||
| const schemas = config.searchPath.split(',').map(s => s.trim()).filter(s => s.length > 0); | ||||||||||||||||||||||||||||||||||||||||
| if (schemas.length > 0) { | ||||||||||||||||||||||||||||||||||||||||
| this.defaultSchema = schemas[0]; | ||||||||||||||||||||||||||||||||||||||||
| const quotedSchemas = schemas.map(s => quoteIdentifier(s, 'postgres')); | ||||||||||||||||||||||||||||||||||||||||
| // Escape backslashes then spaces for PostgreSQL options string parser | ||||||||||||||||||||||||||||||||||||||||
| const optionsValue = quotedSchemas.join(',').replace(/\\/g, '\\\\').replace(/ /g, '\\ '); | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+142
to
+144
|
||||||||||||||||||||||||||||||||||||||||
| const quotedSchemas = schemas.map(s => quoteIdentifier(s, 'postgres')); | |
| // Escape backslashes then spaces for PostgreSQL options string parser | |
| const optionsValue = quotedSchemas.join(',').replace(/\\/g, '\\\\').replace(/ /g, '\\ '); | |
| // For PostgreSQL, avoid unconditionally quoting identifiers: | |
| // - Allow special token $user to pass through unquoted. | |
| // - Leave simple safe identifiers unquoted. | |
| // - Quote everything else to preserve safety. | |
| const safeUnquotedIdentifier = /^[a-zA-Z_][a-zA-Z0-9_$]*$/; | |
| const formattedSchemas = schemas.map((s) => { | |
| if (s === "$user") { | |
| return s; | |
| } | |
| if (safeUnquotedIdentifier.test(s)) { | |
| return s; | |
| } | |
| return quoteIdentifier(s, "postgres"); | |
| }); | |
| // Escape backslashes then spaces for PostgreSQL options string parser | |
| const optionsValue = formattedSchemas.join(',').replace(/\\/g, '\\\\').replace(/ /g, '\\ '); |
Uh oh!
There was an error while loading. Please reload this page.