RefreshS3Secret replaces the DuckDB S3 secret with updated credentials. Used when a hot-idle worker is reclaimed and STS credentials have rotated. Respects the configured S3 provider (config, aws_sdk, credential_chain).
(db *sql.DB, dlCfg DuckLakeConfig, duckLakeSem chan struct{})
| 2027 | stmt: "CREATE INDEX IF NOT EXISTS idx_ducklake_schema_versions_tbl_schema_version ON ducklake_schema_versions (table_id, schema_version)", |
| 2028 | }, |
| 2029 | } |
| 2030 | |
| 2031 | // ensureDuckLakeMetadataIndexes connects directly to the DuckLake PostgreSQL |
| 2032 | // metadata store and creates indexes that dramatically improve query planning |
| 2033 | // performance. This is non-fatal — if it fails, DuckLake still works, just slower. |
| 2034 | // Retries on subsequent AttachDuckLake calls until it succeeds. |
| 2035 | func ensureDuckLakeMetadataIndexes(dlCfg DuckLakeConfig) { |
| 2036 | if duckLakeIndexDone.Load() { |
| 2037 | return |
| 2038 | } |
| 2039 | |
| 2040 | // Only relevant for PostgreSQL metadata stores. |
| 2041 | if !strings.HasPrefix(dlCfg.MetadataStore, "postgres:") { |
| 2042 | return |
| 2043 | } |
| 2044 | |
| 2045 | // Serialize concurrent attempts (multiple connections attaching simultaneously). |
| 2046 | duckLakeIndexMu.Lock() |
| 2047 | defer duckLakeIndexMu.Unlock() |
| 2048 | |
| 2049 | // Double-check after acquiring the lock. |
| 2050 | if duckLakeIndexDone.Load() { |
| 2051 | return |
| 2052 | } |
| 2053 | |
| 2054 | // Strip the "postgres:" DuckLake protocol prefix to get a standard libpq connection string. |
| 2055 | connStr := strings.TrimPrefix(dlCfg.MetadataStore, "postgres:") |
| 2056 | |
| 2057 | // pgx/stdlib accepts libpq key=value format directly. |
| 2058 | pgDB, err := sql.Open("pgx", connStr) |
| 2059 | if err != nil { |
| 2060 | slog.Warn("Failed to open connection for DuckLake metadata indexes.", "error", err) |
| 2061 | return |
| 2062 | } |
| 2063 | defer func() { _ = pgDB.Close() }() |
| 2064 | |
| 2065 | // Fast path: a single pg_indexes lookup avoids 9 CREATE INDEX round-trips |
| 2066 | // when all expected indexes already exist. Each CREATE INDEX IF NOT EXISTS |
| 2067 | // is a no-op at the storage layer but still costs a server round-trip; under |
| 2068 | // pgbouncer transaction pooling that round-trip can take 1-2s during burst |
| 2069 | // load (server-conn handover + TLS handshake to RDS). Collapsing the check |
| 2070 | // to one round-trip cuts the post-attach window from ~16s to a few hundred |
| 2071 | // ms in the steady state. |
| 2072 | expectedNames := make([]string, len(duckLakeMetadataIndexes)) |
| 2073 | for i, ix := range duckLakeMetadataIndexes { |
| 2074 | expectedNames[i] = ix.name |