@malloy-publisher/server 0.9.5 → 0.9.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5676,8 +5676,9 @@ components:
5676
5676
  type: string
5677
5677
  enum: [disable, no-verify, verify-ca, verify-full]
5678
5678
  description:
5679
- TLS mode for a connection reached through a `proxy` (SSH bastion).
5680
- Because the driver connects to a local tunnel endpoint, the cert
5679
+ TLS mode for the connection. On a connection reached through a
5680
+ `proxy` (SSH bastion), the driver connects to a local tunnel
5681
+ endpoint, so the cert
5681
5682
  hostname can't be checked from the tunnel address alone; use
5682
5683
  `verify-full` to check it against the real host. `verify-ca`
5683
5684
  validates the server cert chain against the trusted CA bundle (e.g.
@@ -5694,9 +5695,20 @@ components:
5694
5695
  superset; it adds the hostname check without narrowing CA trust.
5695
5696
  The server defaults it to
5696
5697
  `no-verify` when a proxy is set (so a force-SSL target isn't rejected
5697
- for plaintext) — a server-applied default, not a schema default. Only
5698
- valid on a proxied connection — a direct connection uses the
5699
- deployment PGSSLMODE and rejects this field.
5698
+ for plaintext) — a server-applied default, not a schema default. On
5699
+ a direct connection the same modes apply against the configured
5700
+ host; when omitted, the deployment PGSSLMODE applies. Ignored when
5701
+ connectionString is set, which carries its own sslmode. Not applied
5702
+ to a DuckLake catalog connection.
5703
+ statementTimeoutMilliseconds:
5704
+ type: integer
5705
+ minimum: 1
5706
+ maximum: 2147483647
5707
+ description:
5708
+ Maximum run time of each statement on the database, in
5709
+ milliseconds. The database cancels a statement that exceeds it.
5710
+ When omitted, the database's own statement_timeout applies. Not
5711
+ applied to a DuckLake catalog connection.
5700
5712
 
5701
5713
  MysqlConnection:
5702
5714
  type: object
@@ -1115,7 +1115,7 @@ function installRecordPath(environmentPath, packageName) {
1115
1115
  function normalizeModelPath(p) {
1116
1116
  return p.replace(/\\/g, "/").replace(/^\.\//, "");
1117
1117
  }
1118
- var API_PREFIX = "/api/v0", README_NAME = "README.md", PUBLISHER_CONFIG_NAME = "publisher.config.json", PACKAGE_MANIFEST_NAME = "publisher.json", PACKAGE_INSTALL_RECORDS_DIR = ".install-records", MODEL_FILE_SUFFIX = ".malloy", NOTEBOOK_FILE_SUFFIX = ".malloynb", INDEX_MODEL_NAME = "index.malloy", DEFAULT_QUERY_ROW_LIMIT = 1000, DEFAULT_MAX_QUERY_ROWS = 1e5, DEFAULT_MAX_RESPONSE_BYTES = 50000000, DEFAULT_QUERY_TIMEOUT_MS = 300000, DEFAULT_MAX_CONCURRENT_QUERIES = 32, TEMP_DIR_PATH, PUBLISHER_DATA_DIR = "publisher_data";
1118
+ var API_PREFIX = "/api/v0", README_NAME = "README.md", PUBLISHER_CONFIG_NAME = "publisher.config.json", PACKAGE_MANIFEST_NAME = "publisher.json", PACKAGE_INSTALL_RECORDS_DIR = ".install-records", MODEL_FILE_SUFFIX = ".malloy", NOTEBOOK_FILE_SUFFIX = ".malloynb", INDEX_MODEL_NAME = "index.malloy", DEFAULT_QUERY_ROW_LIMIT = 1000, DEFAULT_MAX_QUERY_ROWS = 1e5, DEFAULT_MAX_RESPONSE_BYTES = 50000000, DEFAULT_QUERY_TIMEOUT_MS = 300000, DEFAULT_MAX_CONCURRENT_QUERIES = 32, DEFAULT_POSTGRES_POOL_MAX = 5, TEMP_DIR_PATH, PUBLISHER_DATA_DIR = "publisher_data";
1119
1119
  var init_constants = __esm(() => {
1120
1120
  TEMP_DIR_PATH = os.tmpdir();
1121
1121
  });
@@ -13199,7 +13199,7 @@ function toQueryTextProblem(problem) {
13199
13199
  ...problem.at ? { at: { range: problem.at.range } } : {}
13200
13200
  };
13201
13201
  }
13202
- var GENERIC_INTERNAL_MESSAGE = "Internal server error.", GENERIC_UPSTREAM_MESSAGE = "Upstream connection error.", MAX_LOGGED_DETAIL_CHARS = 2000, FILESYSTEM_ACCESS_DESCRIPTIONS, NotImplementedError, BadRequestError, InvalidArgumentError, CompileRefusedError, RenderTagRefusedError, UnparseableTextError, EnvironmentNotFoundError, PackageNotFoundError, ModelNotFoundError, DashboardNotFoundError, ConnectionNotFoundError, TableNotFoundError, ConnectionError, DestinationNotFoundError, ConnectionAuthError, UnsupportedCatalogFormatError, ModelCompilationError, PackageManifestError, MaterializationEligibilityError, ChainedUpstreamMissingError, ChainedShapeNotCarriedError, PublisherConfigError, ServerConfigurationError, FrozenConfigError, AccessDeniedError, QueryCompileError, NotQueryableError, OffSurfaceError, MaterializationNotFoundError, MaterializationConflictError, WriteConflictError, WriteRolledBackError, WriteVerifyError, InvalidStateTransitionError, ServiceUnavailableError, PackageAdmissionRefusedError, PayloadTooLargeError, ResponseUnserializableError, QueryTimeoutError;
13202
+ var GENERIC_INTERNAL_MESSAGE = "Internal server error.", GENERIC_UPSTREAM_MESSAGE = "Upstream connection error.", MAX_LOGGED_DETAIL_CHARS = 2000, FILESYSTEM_ACCESS_DESCRIPTIONS, NotImplementedError, BadRequestError, InvalidArgumentError, CompileRefusedError, RenderTagRefusedError, UnparseableTextError, EnvironmentNotFoundError, PackageNotFoundError, ModelNotFoundError, DashboardNotFoundError, ConnectionNotFoundError, TableNotFoundError, ConnectionError, ConnectionPoolExhaustedError, DestinationNotFoundError, ConnectionAuthError, UnsupportedCatalogFormatError, ModelCompilationError, PackageManifestError, MaterializationEligibilityError, ChainedUpstreamMissingError, ChainedShapeNotCarriedError, PublisherConfigError, ServerConfigurationError, FrozenConfigError, AccessDeniedError, QueryCompileError, NotQueryableError, OffSurfaceError, MaterializationNotFoundError, MaterializationConflictError, WriteConflictError, WriteRolledBackError, WriteVerifyError, InvalidStateTransitionError, ServiceUnavailableError, PackageAdmissionRefusedError, PayloadTooLargeError, ResponseUnserializableError, QueryTimeoutError;
13203
13203
  var init_errors = __esm(() => {
13204
13204
  init_constants();
13205
13205
  init_logger();
@@ -13268,6 +13268,12 @@ var init_errors = __esm(() => {
13268
13268
  this.callerSafe = options?.callerSafe ?? false;
13269
13269
  }
13270
13270
  };
13271
+ ConnectionPoolExhaustedError = class ConnectionPoolExhaustedError extends ConnectionError {
13272
+ constructor(message) {
13273
+ super(message, { callerSafe: true });
13274
+ this.name = "ConnectionPoolExhaustedError";
13275
+ }
13276
+ };
13271
13277
  DestinationNotFoundError = class DestinationNotFoundError extends Error {
13272
13278
  constructor(message) {
13273
13279
  super(message);
package/dist/server.mjs CHANGED
@@ -205143,7 +205143,7 @@ function installRecordPath(environmentPath, packageName) {
205143
205143
  function normalizeModelPath(p) {
205144
205144
  return p.replace(/\\/g, "/").replace(/^\.\//, "");
205145
205145
  }
205146
- var API_PREFIX = "/api/v0", README_NAME = "README.md", PUBLISHER_CONFIG_NAME = "publisher.config.json", PACKAGE_MANIFEST_NAME = "publisher.json", PACKAGE_INSTALL_RECORDS_DIR = ".install-records", MODEL_FILE_SUFFIX = ".malloy", NOTEBOOK_FILE_SUFFIX = ".malloynb", INDEX_MODEL_NAME = "index.malloy", DEFAULT_QUERY_ROW_LIMIT = 1000, DEFAULT_MAX_QUERY_ROWS = 1e5, DEFAULT_MAX_RESPONSE_BYTES = 50000000, DEFAULT_QUERY_TIMEOUT_MS = 300000, DEFAULT_MAX_CONCURRENT_QUERIES = 32, TEMP_DIR_PATH, PUBLISHER_DATA_DIR = "publisher_data";
205146
+ var API_PREFIX = "/api/v0", README_NAME = "README.md", PUBLISHER_CONFIG_NAME = "publisher.config.json", PACKAGE_MANIFEST_NAME = "publisher.json", PACKAGE_INSTALL_RECORDS_DIR = ".install-records", MODEL_FILE_SUFFIX = ".malloy", NOTEBOOK_FILE_SUFFIX = ".malloynb", INDEX_MODEL_NAME = "index.malloy", DEFAULT_QUERY_ROW_LIMIT = 1000, DEFAULT_MAX_QUERY_ROWS = 1e5, DEFAULT_MAX_RESPONSE_BYTES = 50000000, DEFAULT_QUERY_TIMEOUT_MS = 300000, DEFAULT_MAX_CONCURRENT_QUERIES = 32, DEFAULT_POSTGRES_POOL_MAX = 5, TEMP_DIR_PATH, PUBLISHER_DATA_DIR = "publisher_data";
205147
205147
  var init_constants = __esm(() => {
205148
205148
  TEMP_DIR_PATH = os.tmpdir();
205149
205149
  });
@@ -205300,7 +205300,7 @@ function toQueryTextProblem(problem) {
205300
205300
  ...problem.at ? { at: { range: problem.at.range } } : {}
205301
205301
  };
205302
205302
  }
205303
- var GENERIC_INTERNAL_MESSAGE = "Internal server error.", GENERIC_UPSTREAM_MESSAGE = "Upstream connection error.", MAX_LOGGED_DETAIL_CHARS = 2000, FILESYSTEM_ACCESS_DESCRIPTIONS, NotImplementedError, BadRequestError, InvalidArgumentError, CompileRefusedError, RenderTagRefusedError, UnparseableTextError, EnvironmentNotFoundError, PackageNotFoundError, ModelNotFoundError, DashboardNotFoundError, ConnectionNotFoundError, TableNotFoundError, ConnectionError, DestinationNotFoundError, ConnectionAuthError, UnsupportedCatalogFormatError, ModelCompilationError, PackageManifestError, MaterializationEligibilityError, ChainedUpstreamMissingError, ChainedShapeNotCarriedError, PublisherConfigError, ServerConfigurationError, FrozenConfigError, AccessDeniedError, QueryCompileError, NotQueryableError, OffSurfaceError, MaterializationNotFoundError, MaterializationConflictError, WriteConflictError, WriteRolledBackError, WriteVerifyError, InvalidStateTransitionError, ServiceUnavailableError, PackageAdmissionRefusedError, PayloadTooLargeError, ResponseUnserializableError, QueryTimeoutError;
205303
+ var GENERIC_INTERNAL_MESSAGE = "Internal server error.", GENERIC_UPSTREAM_MESSAGE = "Upstream connection error.", MAX_LOGGED_DETAIL_CHARS = 2000, FILESYSTEM_ACCESS_DESCRIPTIONS, NotImplementedError, BadRequestError, InvalidArgumentError, CompileRefusedError, RenderTagRefusedError, UnparseableTextError, EnvironmentNotFoundError, PackageNotFoundError, ModelNotFoundError, DashboardNotFoundError, ConnectionNotFoundError, TableNotFoundError, ConnectionError, ConnectionPoolExhaustedError, DestinationNotFoundError, ConnectionAuthError, UnsupportedCatalogFormatError, ModelCompilationError, PackageManifestError, MaterializationEligibilityError, ChainedUpstreamMissingError, ChainedShapeNotCarriedError, PublisherConfigError, ServerConfigurationError, FrozenConfigError, AccessDeniedError, QueryCompileError, NotQueryableError, OffSurfaceError, MaterializationNotFoundError, MaterializationConflictError, WriteConflictError, WriteRolledBackError, WriteVerifyError, InvalidStateTransitionError, ServiceUnavailableError, PackageAdmissionRefusedError, PayloadTooLargeError, ResponseUnserializableError, QueryTimeoutError;
205304
205304
  var init_errors = __esm(() => {
205305
205305
  init_constants();
205306
205306
  init_logger();
@@ -205369,6 +205369,12 @@ var init_errors = __esm(() => {
205369
205369
  this.callerSafe = options?.callerSafe ?? false;
205370
205370
  }
205371
205371
  };
205372
+ ConnectionPoolExhaustedError = class ConnectionPoolExhaustedError extends ConnectionError {
205373
+ constructor(message) {
205374
+ super(message, { callerSafe: true });
205375
+ this.name = "ConnectionPoolExhaustedError";
205376
+ }
205377
+ };
205372
205378
  DestinationNotFoundError = class DestinationNotFoundError extends Error {
205373
205379
  constructor(message) {
205374
205380
  super(message);
@@ -206423,6 +206429,14 @@ var BUNDLED_DEFAULT_CONFIG_PATH, PER_MODE_COLOR_KEYS, DEFAULT_HIGH_WATER_FRACTIO
206423
206429
  throw new Error(`PUBLISHER_MAX_CONCURRENT_QUERIES must be a non-negative integer (got ${raw})`);
206424
206430
  }
206425
206431
  return raw;
206432
+ }, getPostgresPoolMax = () => {
206433
+ const raw = parseIntEnv("PUBLISHER_POSTGRES_POOL_MAX");
206434
+ if (raw === undefined)
206435
+ return DEFAULT_POSTGRES_POOL_MAX;
206436
+ if (raw < 1) {
206437
+ throw new Error(`PUBLISHER_POSTGRES_POOL_MAX must be a positive integer (got ${raw})`);
206438
+ }
206439
+ return raw;
206426
206440
  }, getExtensionFetchPolicy = () => {
206427
206441
  const raw = process.env.EXTENSION_FETCH_POLICY;
206428
206442
  if (raw === undefined || raw.trim() === "")
@@ -283563,6 +283577,26 @@ var init_query_metadata = __esm(() => {
283563
283577
  import { createPrivateKey } from "crypto";
283564
283578
  import { existsSync } from "fs";
283565
283579
  import path4 from "path";
283580
+ function validatePostgresSslmode(subject, sslmode) {
283581
+ if (!PROXIED_SSLMODES.includes(sslmode)) {
283582
+ throw new Error(`${subject} has unsupported sslmode '${sslmode}' ` + `(expected ${PROXIED_SSLMODES.join(" | ")}).`);
283583
+ }
283584
+ if (sslmode === "verify-ca") {
283585
+ const caBundle = process.env.NODE_EXTRA_CA_CERTS;
283586
+ if (!caBundle || !existsSync(caBundle)) {
283587
+ throw new Error(`${subject} uses sslmode 'verify-ca' but no readable ` + `CA bundle is available (NODE_EXTRA_CA_CERTS is unset or points at a missing file). ` + `Add the CA bundle to the image or use sslmode 'no-verify'.`);
283588
+ }
283589
+ }
283590
+ }
283591
+ function postgresStatementTimeoutMs(name, pg) {
283592
+ const value = pg.statementTimeoutMilliseconds;
283593
+ if (value === undefined || value === null)
283594
+ return;
283595
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 1 || value > POSTGRES_MAX_STATEMENT_TIMEOUT_MS) {
283596
+ throw new Error(`Connection '${name}' has an invalid statementTimeoutMilliseconds ` + `${JSON.stringify(value)} (expected an integer from 1 to ${POSTGRES_MAX_STATEMENT_TIMEOUT_MS}).`);
283597
+ }
283598
+ return value;
283599
+ }
283566
283600
  function nullToUndefined(value) {
283567
283601
  return value ?? undefined;
283568
283602
  }
@@ -283657,7 +283691,7 @@ function getStaticConnectionAttributes(type) {
283657
283691
  case "postgres":
283658
283692
  return {
283659
283693
  dialectName: "postgres",
283660
- isPool: false,
283694
+ isPool: true,
283661
283695
  canPersist: true,
283662
283696
  canStream: true
283663
283697
  };
@@ -283724,12 +283758,34 @@ function parseServiceAccountKey(json2) {
283724
283758
  }
283725
283759
  return keyData;
283726
283760
  }
283727
- function buildPostgresConnectionString(config2) {
283728
- if (config2.connectionString || !process.env.PGSSLMODE) {
283761
+ function directSslParams(sslmode) {
283762
+ switch (sslmode) {
283763
+ case "disable":
283764
+ case "no-verify":
283765
+ case "verify-full":
283766
+ return { sslmode };
283767
+ case "verify-ca":
283768
+ return {
283769
+ uselibpqcompat: "true",
283770
+ sslmode: "verify-ca",
283771
+ sslrootcert: process.env.NODE_EXTRA_CA_CERTS ?? ""
283772
+ };
283773
+ }
283774
+ }
283775
+ function buildPostgresConnectionString(config2, { applySslmode }) {
283776
+ if (config2.connectionString) {
283729
283777
  return config2.connectionString;
283730
283778
  }
283731
283779
  const params = new URLSearchParams;
283732
- params.set("sslmode", process.env.PGSSLMODE);
283780
+ if (applySslmode && config2.sslmode != null) {
283781
+ for (const [key, value] of Object.entries(directSslParams(config2.sslmode))) {
283782
+ params.set(key, value);
283783
+ }
283784
+ } else if (process.env.PGSSLMODE) {
283785
+ params.set("sslmode", process.env.PGSSLMODE);
283786
+ } else {
283787
+ return;
283788
+ }
283733
283789
  const auth = config2.userName && config2.password ? `${encodeURIComponent(config2.userName)}:${encodeURIComponent(config2.password)}@` : config2.userName ? `${encodeURIComponent(config2.userName)}@` : "";
283734
283790
  const host = config2.host ?? "localhost";
283735
283791
  const port = config2.port ? `:${config2.port}` : "";
@@ -283800,23 +283856,21 @@ function validateConnectionShape(connection) {
283800
283856
  }
283801
283857
  const sslmode = connection.postgresConnection?.sslmode;
283802
283858
  if (sslmode != null) {
283803
- if (!PROXIED_SSLMODES.includes(sslmode)) {
283804
- throw new Error(`Connection proxy on '${connection.name}' has unsupported sslmode '${sslmode}' ` + `(expected ${PROXIED_SSLMODES.join(" | ")}).`);
283805
- }
283806
- if (sslmode === "verify-ca") {
283807
- const caBundle = process.env.NODE_EXTRA_CA_CERTS;
283808
- if (!caBundle || !existsSync(caBundle)) {
283809
- throw new Error(`Connection proxy on '${connection.name}' uses sslmode 'verify-ca' but no readable ` + `CA bundle is available (NODE_EXTRA_CA_CERTS is unset or points at a missing file). ` + `Add the CA bundle to the image or use sslmode 'no-verify'.`);
283810
- }
283811
- }
283859
+ validatePostgresSslmode(`Connection proxy on '${connection.name}'`, sslmode);
283812
283860
  }
283813
283861
  const dbName = connection.postgresConnection?.databaseName;
283814
283862
  if (dbName && decodeURI(encodeURIComponent(dbName)) !== dbName) {
283815
283863
  throw new Error(`Connection proxy on '${connection.name}' has a database name with characters that can't ` + `be carried over a proxied connection (${JSON.stringify(dbName)}). Use a database name ` + `without URI-reserved characters (; , / ? : @ & = + $ #).`);
283816
283864
  }
283817
283865
  }
283818
- if (!connection.proxy && connection.postgresConnection?.sslmode) {
283819
- throw new Error(`Connection '${connection.name}' sets postgresConnection.sslmode but has no proxy; sslmode is ` + `only supported for proxied connections (direct connections use the deployment PGSSLMODE).`);
283866
+ if (!connection.proxy && connection.postgresConnection?.sslmode != null) {
283867
+ validatePostgresSslmode(`Connection '${connection.name}'`, connection.postgresConnection.sslmode);
283868
+ if (connection.postgresConnection.connectionString) {
283869
+ logger.warn(`Connection '${connection.name}' sets both postgresConnection.sslmode and ` + `connectionString; sslmode is ignored and the connectionString's own sslmode applies.`);
283870
+ }
283871
+ }
283872
+ if (connection.postgresConnection) {
283873
+ postgresStatementTimeoutMs(connection.name ?? "", connection.postgresConnection);
283820
283874
  }
283821
283875
  switch (connection.type) {
283822
283876
  case "postgres":
@@ -284113,7 +284167,9 @@ function assembleEnvironmentConnections(connections = [], environmentPath = "")
284113
284167
  username: postgresConnection?.userName,
284114
284168
  password: postgresConnection?.password,
284115
284169
  databaseName: postgresConnection?.databaseName,
284116
- connectionString: postgresConnection ? buildPostgresConnectionString(postgresConnection) : undefined
284170
+ connectionString: postgresConnection ? buildPostgresConnectionString(postgresConnection, {
284171
+ applySslmode: !connection.proxy
284172
+ }) : undefined
284117
284173
  };
284118
284174
  break;
284119
284175
  }
@@ -284252,7 +284308,7 @@ function validateAndBuildTrinoCoreConfig(trinoConfig) {
284252
284308
  }
284253
284309
  throw new Error(`Invalid Trino connection: expected "http://server:port" or "https://server:port".`);
284254
284310
  }
284255
- var PROXIED_SSLMODES, PUBLISHER_DUCKDB_API_FIELDS, DECLARABLE_STORAGE_DESTINATION_TYPES, STORAGE_DESTINATIONS_DIR = ".storage-destinations";
284311
+ var PROXIED_SSLMODES, POSTGRES_MAX_STATEMENT_TIMEOUT_MS = 2147483647, PUBLISHER_DUCKDB_API_FIELDS, DECLARABLE_STORAGE_DESTINATION_TYPES, STORAGE_DESTINATIONS_DIR = ".storage-destinations";
284256
284312
  var init_connection_config = __esm(() => {
284257
284313
  init_config();
284258
284314
  init_errors();
@@ -294890,9 +294946,117 @@ async function attachSnowflake(connection, attachedDb) {
294890
294946
  await connection.runSQL(attachCommand);
294891
294947
  logger.info(`Successfully attached Snowflake database: ${attachedDb.name}`);
294892
294948
  }
294893
- function buildPgConnectionString(pg) {
294949
+ function mergeStatementTimeoutOption(existing, ms) {
294950
+ const tokens = existing.split(/(?<!\\)\s+/).filter((t2) => t2 !== "");
294951
+ const kept = [];
294952
+ let replaced = false;
294953
+ for (let i2 = 0;i2 < tokens.length; i2++) {
294954
+ if (tokens[i2] === "-c" && /^statement[_-]timeout=/i.test(tokens[i2 + 1] ?? "")) {
294955
+ i2++;
294956
+ replaced = true;
294957
+ } else if (STATEMENT_TIMEOUT_SWITCH.test(tokens[i2])) {
294958
+ replaced = true;
294959
+ } else {
294960
+ kept.push(tokens[i2]);
294961
+ }
294962
+ }
294963
+ kept.push("-c", `statement_timeout=${ms}`);
294964
+ return { value: kept.join(" "), replaced };
294965
+ }
294966
+ function conninfoPairs(conninfo) {
294967
+ const pairs = [];
294968
+ const isSpace = (c) => c !== undefined && /\s/.test(c);
294969
+ let i2 = 0;
294970
+ while (i2 < conninfo.length) {
294971
+ while (isSpace(conninfo[i2]))
294972
+ i2++;
294973
+ if (i2 >= conninfo.length)
294974
+ break;
294975
+ const start = i2;
294976
+ let key = "";
294977
+ while (i2 < conninfo.length && conninfo[i2] !== "=" && !isSpace(conninfo[i2])) {
294978
+ key += conninfo[i2++];
294979
+ }
294980
+ while (isSpace(conninfo[i2]))
294981
+ i2++;
294982
+ if (conninfo[i2] !== "=")
294983
+ break;
294984
+ i2++;
294985
+ while (isSpace(conninfo[i2]))
294986
+ i2++;
294987
+ let value = "";
294988
+ if (conninfo[i2] === "'") {
294989
+ i2++;
294990
+ while (i2 < conninfo.length && conninfo[i2] !== "'") {
294991
+ if (conninfo[i2] === "\\" && i2 + 1 < conninfo.length)
294992
+ i2++;
294993
+ value += conninfo[i2++];
294994
+ }
294995
+ i2++;
294996
+ } else {
294997
+ while (i2 < conninfo.length && !isSpace(conninfo[i2])) {
294998
+ if (conninfo[i2] === "\\" && i2 + 1 < conninfo.length)
294999
+ i2++;
295000
+ value += conninfo[i2++];
295001
+ }
295002
+ }
295003
+ pairs.push({ key, value, start, end: Math.min(i2, conninfo.length) });
295004
+ }
295005
+ return pairs;
295006
+ }
295007
+ function decodeUriComponentOrRaw(value) {
295008
+ try {
295009
+ return decodeURIComponent(value);
295010
+ } catch {
295011
+ return value;
295012
+ }
295013
+ }
295014
+ function withLibpqStatementTimeout(name, connectionString, pg) {
295015
+ const ms = postgresStatementTimeoutMs(name, pg);
295016
+ if (ms === undefined)
295017
+ return connectionString;
295018
+ const merged = (existing2) => {
295019
+ if (existing2 === undefined)
295020
+ return `-c statement_timeout=${ms}`;
295021
+ const { value, replaced } = mergeStatementTimeoutOption(existing2, ms);
295022
+ if (replaced) {
295023
+ logger.warn(`Connection '${name}' sets statementTimeoutMilliseconds and its connectionString's ` + `options also set statement_timeout; statementTimeoutMilliseconds applies.`);
295024
+ }
295025
+ return value;
295026
+ };
295027
+ if (/^postgres(ql)?:\/\//i.test(connectionString)) {
295028
+ const queryStart = connectionString.indexOf("?");
295029
+ if (queryStart === -1) {
295030
+ return `${connectionString}?options=${encodeURIComponent(merged(undefined))}`;
295031
+ }
295032
+ const params = connectionString.slice(queryStart + 1).split("&");
295033
+ const isOptions = (param) => decodeUriComponentOrRaw(param.split("=", 1)[0]) === "options";
295034
+ const last = params.map(isOptions).lastIndexOf(true);
295035
+ if (last === -1) {
295036
+ params.push(`options=${encodeURIComponent(merged(undefined))}`);
295037
+ } else {
295038
+ const raw = params[last].slice(params[last].indexOf("=") + 1);
295039
+ params[last] = `options=${encodeURIComponent(merged(decodeUriComponentOrRaw(raw)))}`;
295040
+ }
295041
+ return `${connectionString.slice(0, queryStart)}?${params.join("&")}`;
295042
+ }
295043
+ const existing = conninfoPairs(connectionString).filter((pair) => pair.key === "options").pop();
295044
+ if (!existing) {
295045
+ return `${connectionString} ${pgConninfoPair("options", merged(undefined))}`;
295046
+ }
295047
+ return connectionString.slice(0, existing.start) + pgConninfoPair("options", merged(existing.value)) + connectionString.slice(existing.end);
295048
+ }
295049
+ function buildPgConnectionString(pg, {
295050
+ name,
295051
+ applyConnectionOptions
295052
+ }) {
294894
295053
  if (pg.connectionString) {
294895
- return pg.connectionString;
295054
+ if (!applyConnectionOptions)
295055
+ return pg.connectionString;
295056
+ if (pg.sslmode != null) {
295057
+ logger.warn(`Connection '${name}' sets both postgresConnection.sslmode and ` + `connectionString; sslmode is ignored and the connectionString's own sslmode applies.`);
295058
+ }
295059
+ return withLibpqStatementTimeout(name, pg.connectionString, pg);
294896
295060
  }
294897
295061
  const parts = [];
294898
295062
  if (pg.host)
@@ -294905,6 +295069,13 @@ function buildPgConnectionString(pg) {
294905
295069
  parts.push(`user=${pg.userName}`);
294906
295070
  if (pg.password)
294907
295071
  parts.push(`password=${pg.password}`);
295072
+ if (applyConnectionOptions) {
295073
+ parts.push(...libpqStatementTimeoutParts(name, pg));
295074
+ if (pg.sslmode != null) {
295075
+ parts.push(...libpqSslmodeParts(`Connection '${name}'`, pg.sslmode, defaultProxiedAttachTrust()));
295076
+ return parts.join(" ");
295077
+ }
295078
+ }
294908
295079
  const pgSSLMode = process.env.PGSSLMODE;
294909
295080
  if (pgSSLMode) {
294910
295081
  const mapping = {
@@ -294931,7 +295102,10 @@ async function attachPostgres(connection, attachedDb) {
294931
295102
  }
294932
295103
  await installAndLoadExtension(connection, "postgres");
294933
295104
  const config2 = attachedDb.postgresConnection;
294934
- const attachString = buildPgConnectionString(config2);
295105
+ const attachString = buildPgConnectionString(config2, {
295106
+ name: attachedDb.name,
295107
+ applyConnectionOptions: true
295108
+ });
294935
295109
  const attachCommand = `ATTACH '${escapeSQL(attachString)}' AS ${quoteIdentifier(attachedDb.name, "duckdb")} (TYPE postgres, READ_ONLY);`;
294936
295110
  await connection.runSQL(attachCommand);
294937
295111
  logger.info(`Successfully attached PostgreSQL database: ${attachedDb.name}`);
@@ -295026,7 +295200,10 @@ async function attachDuckLakeWithMode(connection, dbName, ducklakeConfig, option
295026
295200
  });
295027
295201
  }
295028
295202
  const pg = ducklakeConfig.catalog.postgresConnection;
295029
- const pgConnString = buildPgConnectionString(pg);
295203
+ const pgConnString = buildPgConnectionString(pg, {
295204
+ name: dbName,
295205
+ applyConnectionOptions: false
295206
+ });
295030
295207
  const mode = options.readOnly ? "READ_ONLY" : "READ_WRITE";
295031
295208
  logger.debug(`pgConnString: ${redactPgSecrets(pgConnString)}`);
295032
295209
  const escapedPgConnString = escapeSQL(pgConnString);
@@ -295193,7 +295370,10 @@ async function federatePostgres(connection, config2, deps = {}) {
295193
295370
  });
295194
295371
  attachString = buildProxiedPgAttachString(config2.name, pg, endpoint);
295195
295372
  } else {
295196
- attachString = buildPgConnectionString(pg);
295373
+ attachString = buildPgConnectionString(pg, {
295374
+ name: config2.name,
295375
+ applyConnectionOptions: true
295376
+ });
295197
295377
  }
295198
295378
  logger.info(`Federating Postgres source for passthrough as alias '${alias}'${endpoint ? " through its SSH proxy" : ""}: ${redactPgSecrets(attachString)}`);
295199
295379
  await connection.runSQL(`ATTACH OR REPLACE '${escapeSQL(attachString)}' AS ${quoteIdentifier(alias, "duckdb")} (TYPE postgres, READ_ONLY);`);
@@ -295256,30 +295436,39 @@ function buildProxiedPgAttachString(name, pg, endpoint, trust = defaultProxiedAt
295256
295436
  parts.push(pgConninfoPair("user", pg.userName));
295257
295437
  if (pg.password)
295258
295438
  parts.push(pgConninfoPair("password", pg.password));
295259
- const mode = pg.sslmode ?? "no-verify";
295439
+ parts.push(...libpqSslmodeParts(`Connection proxy on '${name}'`, pg.sslmode ?? "no-verify", trust));
295440
+ parts.push(...libpqStatementTimeoutParts(name, pg));
295441
+ return parts.join(" ");
295442
+ }
295443
+ function libpqSslmodeParts(subject, mode, trust) {
295260
295444
  switch (mode) {
295261
295445
  case "disable":
295262
- parts.push("sslmode=disable");
295263
- break;
295446
+ return ["sslmode=disable"];
295264
295447
  case "no-verify":
295265
- parts.push("sslmode=require");
295266
- break;
295448
+ return ["sslmode=require"];
295267
295449
  case "verify-ca": {
295268
295450
  if (!trust.caBundle) {
295269
- throw new Error(`Connection proxy on '${name}' uses sslmode 'verify-ca' but no trusted CA bundle is available (NODE_EXTRA_CA_CERTS is unset).`);
295451
+ throw new Error(`${subject} uses sslmode 'verify-ca' but no trusted CA bundle is available (NODE_EXTRA_CA_CERTS is unset).`);
295270
295452
  }
295271
- parts.push("sslmode=verify-ca", pgConninfoPair("sslrootcert", trust.caBundle));
295272
- break;
295453
+ return [
295454
+ "sslmode=verify-ca",
295455
+ pgConninfoPair("sslrootcert", trust.caBundle)
295456
+ ];
295273
295457
  }
295274
295458
  case "verify-full":
295275
- parts.push("sslmode=verify-full", pgConninfoPair("sslrootcert", trust.ambientBundle()));
295276
- break;
295459
+ return [
295460
+ "sslmode=verify-full",
295461
+ pgConninfoPair("sslrootcert", trust.ambientBundle())
295462
+ ];
295277
295463
  default: {
295278
295464
  const unhandled = mode;
295279
- throw new Error(`Connection proxy on '${name}' has unsupported sslmode '${String(unhandled)}' (expected ${PROXIED_SSLMODES.join(" | ")}).`);
295465
+ throw new Error(`${subject} has unsupported sslmode '${String(unhandled)}' (expected ${PROXIED_SSLMODES.join(" | ")}).`);
295280
295466
  }
295281
295467
  }
295282
- return parts.join(" ");
295468
+ }
295469
+ function libpqStatementTimeoutParts(name, pg) {
295470
+ const ms = postgresStatementTimeoutMs(name, pg);
295471
+ return ms === undefined ? [] : [pgConninfoPair("options", `-c statement_timeout=${ms}`)];
295283
295472
  }
295284
295473
  function shouldRenewStorageSecret(opts) {
295285
295474
  return opts.hasRenewer && !opts.alreadyRenewing && isExpiredCredentialError(opts.error);
@@ -295601,14 +295790,40 @@ function buildProxiedPostgresConnection(metadata, endpoint) {
295601
295790
  const db = pg.databaseName ? `/${enc(pg.databaseName)}` : "";
295602
295791
  const { query, ssl } = resolveProxiedTls(name, pg.host, pg.sslmode);
295603
295792
  const connectionString = `postgresql://${auth}${endpoint.host}:${endpoint.port}${db}${query}`;
295793
+ const setupSQL = statementTimeoutSetupSQL(name, pg);
295604
295794
  return new PooledPostgresConnection({
295605
295795
  name,
295606
295796
  connectionString,
295607
295797
  ...ssl ? { ssl } : {},
295798
+ ...setupSQL ? { setupSQL } : {},
295608
295799
  poolMin: 1,
295609
295800
  poolMax: 20
295610
295801
  });
295611
295802
  }
295803
+ function buildEnvironmentPostgresConnection(metadata) {
295804
+ const name = metadata.apiConnection.name;
295805
+ const pg = metadata.apiConnection.postgresConnection;
295806
+ if (!pg) {
295807
+ throw new Error(`Connection '${name}' has type 'postgres' but no postgresConnection config.`);
295808
+ }
295809
+ const setupSQL = statementTimeoutSetupSQL(name, pg);
295810
+ return new EnvironmentPooledPostgresConnection({
295811
+ name,
295812
+ connectionString: buildPostgresConnectionString(pg, {
295813
+ applySslmode: true
295814
+ }),
295815
+ host: pg.host,
295816
+ port: pg.port,
295817
+ username: pg.userName,
295818
+ password: pg.password,
295819
+ databaseName: pg.databaseName,
295820
+ ...setupSQL ? { setupSQL } : {}
295821
+ });
295822
+ }
295823
+ function statementTimeoutSetupSQL(name, pg) {
295824
+ const ms = postgresStatementTimeoutMs(name, pg);
295825
+ return ms === undefined ? undefined : `SET statement_timeout = ${ms}`;
295826
+ }
295612
295827
  function buildDuckLakeConnection(metadata, entry) {
295613
295828
  return new DuckLakeConnection(entryToDuckDBOptions(metadata.apiConnection.name, entry, metadata.workingDirectory));
295614
295829
  }
@@ -295634,6 +295849,7 @@ function buildEnvironmentMalloyConfig(connections = [], environmentPath = "", is
295634
295849
  const snowflakeJwtCache = new Map;
295635
295850
  const azureDuckDBCache = new Map;
295636
295851
  const proxyConnectionCache = new Map;
295852
+ const postgresConnectionCache = new Map;
295637
295853
  const proxyEndpoints = new Map;
295638
295854
  const attachPromises = new WeakMap;
295639
295855
  const malloyConfig = new MalloyConfig(assembled.pojo, {
@@ -295724,6 +295940,21 @@ function buildEnvironmentMalloyConfig(connections = [], environmentPath = "", is
295724
295940
  await attachOnce(connection2, metadata);
295725
295941
  return connection2;
295726
295942
  }
295943
+ if (metadata?.apiConnection.type === "postgres") {
295944
+ let connectionPromise = postgresConnectionCache.get(name);
295945
+ if (!connectionPromise) {
295946
+ connectionPromise = Promise.resolve(buildEnvironmentPostgresConnection(metadata));
295947
+ postgresConnectionCache.set(name, connectionPromise);
295948
+ connectionPromise.catch(() => {
295949
+ if (postgresConnectionCache.get(name) === connectionPromise) {
295950
+ postgresConnectionCache.delete(name);
295951
+ }
295952
+ });
295953
+ }
295954
+ const connection2 = await connectionPromise;
295955
+ await attachOnce(connection2, metadata);
295956
+ return connection2;
295957
+ }
295727
295958
  const connection = await base.lookupConnection(name);
295728
295959
  if (metadata) {
295729
295960
  await attachOnce(connection, metadata);
@@ -295749,7 +295980,8 @@ function buildEnvironmentMalloyConfig(connections = [], environmentPath = "", is
295749
295980
  ...duckLakeCache.values(),
295750
295981
  ...snowflakeJwtCache.values(),
295751
295982
  ...azureDuckDBCache.values(),
295752
- ...proxyConnectionCache.values()
295983
+ ...proxyConnectionCache.values(),
295984
+ ...postgresConnectionCache.values()
295753
295985
  ];
295754
295986
  const closeResults = await Promise.allSettled([
295755
295987
  malloyConfig.shutdown("close"),
@@ -295763,6 +295995,7 @@ function buildEnvironmentMalloyConfig(connections = [], environmentPath = "", is
295763
295995
  snowflakeJwtCache.clear();
295764
295996
  azureDuckDBCache.clear();
295765
295997
  proxyConnectionCache.clear();
295998
+ postgresConnectionCache.clear();
295766
295999
  proxyEndpoints.clear();
295767
296000
  const failures = [...closeResults, ...endpointResults].filter((result) => result.status === "rejected");
295768
296001
  for (const failure of failures) {
@@ -295945,7 +296178,7 @@ async function testConnectionConfig(connectionConfig) {
295945
296178
  }
295946
296179
  }
295947
296180
  }
295948
- var import_db_publisher, extensionSessionPinned, sessionLimitsApplied, ducklakePreflightSeq = 0, ambientTrustBundles, ambientTrustDir, AzureDuckDBConnection, DuckLakeConnection, CONFIG_SECRET_KEY;
296181
+ var import_db_publisher, extensionSessionPinned, sessionLimitsApplied, STATEMENT_TIMEOUT_SWITCH, ducklakePreflightSeq = 0, ambientTrustBundles, ambientTrustDir, AzureDuckDBConnection, DuckLakeConnection, POSTGRES_POOL_MAX_USES = 1, PG_POOL_WAIT_TIMEOUT_MESSAGE = "timeout exceeded when trying to connect", EnvironmentPooledPostgresConnection, CONFIG_SECRET_KEY;
295949
296182
  var init_connection = __esm(() => {
295950
296183
  init_axios2();
295951
296184
  init_config();
@@ -295963,6 +296196,7 @@ var init_connection = __esm(() => {
295963
296196
  import_db_publisher = __toESM(require_dist6(), 1);
295964
296197
  extensionSessionPinned = new WeakSet;
295965
296198
  sessionLimitsApplied = new WeakMap;
296199
+ STATEMENT_TIMEOUT_SWITCH = /^(?:-c|--)statement[_-]timeout=/i;
295966
296200
  ambientTrustBundles = new Map;
295967
296201
  AzureDuckDBConnection = class AzureDuckDBConnection extends DuckDBConnection {
295968
296202
  azureDatabases;
@@ -296054,6 +296288,38 @@ var init_connection = __esm(() => {
296054
296288
  return result;
296055
296289
  }
296056
296290
  };
296291
+ EnvironmentPooledPostgresConnection = class EnvironmentPooledPostgresConnection extends PooledPostgresConnection {
296292
+ poolLimits() {
296293
+ return { max: getPostgresPoolMax(), connectionTimeoutMillis: 30000 };
296294
+ }
296295
+ buildClientConfig(cfg) {
296296
+ const base = super.buildClientConfig(cfg);
296297
+ return {
296298
+ ...base,
296299
+ ...this.poolLimits(),
296300
+ maxUses: POSTGRES_POOL_MAX_USES,
296301
+ application_name: "malloy-publisher"
296302
+ };
296303
+ }
296304
+ async withTlsHint(op) {
296305
+ try {
296306
+ return await super.withTlsHint(op);
296307
+ } catch (err) {
296308
+ if (err instanceof Error && err.message === PG_POOL_WAIT_TIMEOUT_MESSAGE) {
296309
+ const { max, connectionTimeoutMillis } = this.poolLimits();
296310
+ const waitSeconds = connectionTimeoutMillis / 1000;
296311
+ logger.warn("Postgres connection pool exhausted", {
296312
+ connection: this.name,
296313
+ poolMax: max,
296314
+ waitSeconds,
296315
+ remedy: "fewer concurrent queries on this connection, or raise PUBLISHER_POSTGRES_POOL_MAX"
296316
+ });
296317
+ throw new ConnectionPoolExhaustedError(`Connection '${this.name}' has no free database session: this server opens at most ${max} at a time for it, and none came free within ${waitSeconds} s. Retry once fewer queries are running on this connection.`);
296318
+ }
296319
+ throw err;
296320
+ }
296321
+ }
296322
+ };
296057
296323
  CONFIG_SECRET_KEY = /pass(word)?|secret|private_?key|service_?account|access_?key|token|peaka_?key/i;
296058
296324
  });
296059
296325
 
@@ -328412,6 +328678,9 @@ run: ${sourceName ? `${quoteMalloyIdentifier2(sourceName)} -> ` : ""}${quoteMall
328412
328678
  });
328413
328679
  throw new BadRequestError(err instanceof Error ? err.message : String(err));
328414
328680
  }
328681
+ if (err instanceof ConnectionError) {
328682
+ throw err;
328683
+ }
328415
328684
  if (err instanceof MalloyError7) {
328416
328685
  throw err;
328417
328686
  }
@@ -358133,7 +358402,8 @@ var POSTGRES = {
358133
358402
  port: "scalar",
358134
358403
  databaseName: "scalar",
358135
358404
  userName: "scalar",
358136
- sslmode: "scalar"
358405
+ sslmode: "scalar",
358406
+ statementTimeoutMilliseconds: "scalar"
358137
358407
  };
358138
358408
  var BIGQUERY = {
358139
358409
  defaultProjectId: "scalar",
@@ -359919,7 +360189,7 @@ class ConnectionController {
359919
360189
  try {
359920
360190
  return await streamSqlWithBudget(malloyConnection, sqlStatement, optionsWithSignal, { maxRows, maxBytes });
359921
360191
  } catch (error2) {
359922
- if (error2 instanceof PayloadTooLargeError)
360192
+ if (error2 instanceof PayloadTooLargeError || error2 instanceof ConnectionError)
359923
360193
  throw error2;
359924
360194
  throw new ConnectionError(error2.message);
359925
360195
  }
@@ -359934,6 +360204,8 @@ class ConnectionController {
359934
360204
  try {
359935
360205
  return await malloyConnection.runSQL(sqlStatement, optionsWithSignal);
359936
360206
  } catch (error2) {
360207
+ if (error2 instanceof ConnectionError)
360208
+ throw error2;
359937
360209
  throw new ConnectionError(error2.message);
359938
360210
  }
359939
360211
  }, getQueryTimeoutMs());
@@ -359966,6 +360238,8 @@ class ConnectionController {
359966
360238
  } catch (error2) {
359967
360239
  if (signal.aborted)
359968
360240
  throw error2;
360241
+ if (error2 instanceof ConnectionError)
360242
+ throw error2;
359969
360243
  throw new ConnectionError(error2.message);
359970
360244
  }
359971
360245
  }, getQueryTimeoutMs());
@@ -365238,6 +365512,15 @@ function classifyToolError(operation, identifier, error2) {
365238
365512
  suggestions: [...BACK_PRESSURE_SUGGESTIONS]
365239
365513
  };
365240
365514
  }
365515
+ if (error2 instanceof ConnectionPoolExhaustedError) {
365516
+ return {
365517
+ message: error2.message,
365518
+ suggestions: [
365519
+ "Retry shortly; this clears as other queries on the connection finish.",
365520
+ "If it persists, run fewer queries at once on this connection, or ask the operator to raise PUBLISHER_POSTGRES_POOL_MAX."
365521
+ ]
365522
+ };
365523
+ }
365241
365524
  if (error2 instanceof ResponseUnserializableError) {
365242
365525
  return {
365243
365526
  message: error2.message,
@@ -381636,7 +381919,7 @@ ${query}`);
381636
381919
  // src/service/environment_store.ts
381637
381920
  init_google_credentials();
381638
381921
  // package.json
381639
- var version2 = "0.9.5";
381922
+ var version2 = "0.9.6";
381640
381923
 
381641
381924
  // src/version.ts
381642
381925
  var SERVER_VERSION = version2;
package/package.json CHANGED
@@ -19,7 +19,7 @@
19
19
  "snowflake"
20
20
  ],
21
21
  "homepage": "https://docs.malloydata.dev/documentation/user_guides/publishing/mcp_agents",
22
- "version": "0.9.5",
22
+ "version": "0.9.6",
23
23
  "main": "dist/server.mjs",
24
24
  "bin": {
25
25
  "malloy-publisher": "dist/server.mjs"