@tablecraft-app/plugin-postgres 1.12.0 → 1.13.0

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.
package/index.js CHANGED
@@ -10823,7 +10823,8 @@ var CAPABILITIES$1 = [
10823
10823
  "commands",
10824
10824
  "keyValue",
10825
10825
  "nodes",
10826
- "serverInfo"
10826
+ "serverInfo",
10827
+ "serverTimeout"
10827
10828
  ];
10828
10829
  /** The message that stands for the ones past `SERVER_MESSAGE_LIMIT`. */
10829
10830
  function omittedMessages(count) {
@@ -11025,6 +11026,11 @@ function urlSetsDeclared(connection) {
11025
11026
  if (url?.sets === void 0) return true;
11026
11027
  return Object.entries(url.sets).every(([scheme, values]) => url.schemes.includes(scheme) && Object.keys(values).every((key) => connection.fields.some((field) => field.key === key)));
11027
11028
  }
11029
+ /** A database product a plugin speaks; `id` is a simple-icons slug, `name` what a row shows. */
11030
+ var brandSchema = strictObject({
11031
+ id: string$1().regex(/^[a-z0-9_]+$/, { message: "expected a simple-icons slug: lower-case letters, digits and underscores" }),
11032
+ name: string$1().min(1)
11033
+ });
11028
11034
  function includesAny(capabilities, ...wanted) {
11029
11035
  return wanted.some((capability) => capabilities.includes(capability));
11030
11036
  }
@@ -11038,6 +11044,8 @@ strictObject({
11038
11044
  capabilities: array(_enum(CAPABILITIES$1)),
11039
11045
  /** The highlighter the console uses for this plugin's dialect (decision 008). */
11040
11046
  highlight: string$1().min(1),
11047
+ /** The database products the plugin speaks, its default first (decision 128). Absent: rows name the plugin. */
11048
+ brands: array(brandSchema).min(1).refine((brands) => new Set(brands.map((brand) => brand.id)).size === brands.length, { message: "brand ids must be unique" }).optional(),
11041
11049
  /** The connection form, readable without running plugin code (decision 019). */
11042
11050
  connection: strictObject({
11043
11051
  fields: array(connectionFieldSchema).min(1),
@@ -25225,6 +25233,39 @@ function postgresSessions(client, open) {
25225
25233
  terminateSession: (target) => stop(target, terminateStatement, "terminate")
25226
25234
  };
25227
25235
  }
25236
+ // ../../plugins/postgres/src/timeout.ts
25237
+ /**
25238
+ * PostgreSQL's `statement_timeout` covers every statement and, measured against PostgreSQL 17,
25239
+ * keeps counting while a `pg-cursor` result waits to be read: the cursor's `Execute` is sent
25240
+ * without a `Sync`, so the statement is still running (decision 129).
25241
+ */
25242
+ var POSTGRES_SERVER_TIMEOUT = {
25243
+ setting: "statement_timeout",
25244
+ covers: "all",
25245
+ counts: "statement"
25246
+ };
25247
+ var READ_TIMEOUT_SQL = "SHOW statement_timeout";
25248
+ var SET_TIMEOUT_SQL = "SELECT set_config('statement_timeout', $1, false)";
25249
+ /** A statement naming the setting may have changed it behind the plugin's back. */
25250
+ var TIMEOUT_SETTING = /statement_timeout/i;
25251
+ function namesServerTimeout(sql) {
25252
+ return TIMEOUT_SETTING.test(sql);
25253
+ }
25254
+ /** `SHOW`'s units for a time setting, in milliseconds; a bare number is milliseconds already. */
25255
+ var UNIT_MS = {
25256
+ ms: 1,
25257
+ s: 1e3,
25258
+ min: 6e4,
25259
+ h: 36e5,
25260
+ d: 864e5
25261
+ };
25262
+ var DURATION = /^(\d+(?:\.\d+)?)\s*(ms|s|min|h|d)?$/;
25263
+ /** `SHOW statement_timeout` as milliseconds: `5s`, `1min`, `250ms`, `0`; anything unreadable is 0, no limit. */
25264
+ function postgresDurationMs(text) {
25265
+ const match = DURATION.exec(text.trim());
25266
+ if (match === null) return 0;
25267
+ return Math.round(Number(match[1]) * (UNIT_MS[match[2] ?? "ms"] ?? 1));
25268
+ }
25228
25269
  // ../../plugins/postgres/src/tls.ts
25229
25270
  var SSL_MODES = [
25230
25271
  "disable",
@@ -25337,7 +25378,8 @@ var CAPABILITIES = [
25337
25378
  "objects",
25338
25379
  "serverMessages",
25339
25380
  "sessions",
25340
- "routineCall"
25381
+ "routineCall",
25382
+ "serverTimeout"
25341
25383
  ];
25342
25384
  /** Function bodies live in dollar quotes; `BEGIN ATOMIC … END` holds a SQL-standard body. */
25343
25385
  var DIALECT = {
@@ -25433,6 +25475,7 @@ var RELEASE_SQL = "RELEASE SAVEPOINT table_craft_insert";
25433
25475
  var ROLLBACK_TO_SQL = "ROLLBACK TO SAVEPOINT table_craft_insert";
25434
25476
  var OPEN_TRANSACTION_MESSAGE = "A transaction is open; commit or roll it back before switching to auto-commit.";
25435
25477
  var BUSY_MESSAGE = "Another statement is still running in this session.";
25478
+ var CANCELLED_MESSAGE = "The statement was cancelled.";
25436
25479
  var DEFAULT_PORT = 5432;
25437
25480
  var MAX_PORT = 65535;
25438
25481
  var DEFAULT_CONNECT_TIMEOUT_SECONDS = 10;
@@ -25560,6 +25603,18 @@ async function openClient(config, options) {
25560
25603
  }
25561
25604
  return client;
25562
25605
  }
25606
+ function startRunning() {
25607
+ return {
25608
+ cancelRequested: false,
25609
+ startedAt: Date.now(),
25610
+ limitMs: null
25611
+ };
25612
+ }
25613
+ /**
25614
+ * `#appliedTimeoutMs` once a transaction ended after the plugin set the limit: a `ROLLBACK` may
25615
+ * have undone it, so the next statement sets it again whatever it asks for.
25616
+ */
25617
+ var UNKNOWN_TIMEOUT = -1;
25563
25618
  function readBatch(cursor) {
25564
25619
  return new Promise((resolve, reject) => {
25565
25620
  cursor.read(FETCH_ROWS, (error, rows, result) => {
@@ -25607,7 +25662,14 @@ var PostgresSession = class PostgresSession {
25607
25662
  mode: "auto",
25608
25663
  open: false
25609
25664
  };
25665
+ /** The session's own `statement_timeout` in ms, 0 for none; read before the first limited statement. */
25666
+ #ownTimeoutMs = null;
25667
+ /** The limit this plugin last set; null before it set one; `UNKNOWN_TIMEOUT` after a transaction ended. */
25668
+ #appliedTimeoutMs = null;
25610
25669
  serverVersion;
25670
+ /** The manifest's one brand. */
25671
+ serverBrand = "postgresql";
25672
+ serverTimeout = POSTGRES_SERVER_TIMEOUT;
25611
25673
  tls;
25612
25674
  introspector;
25613
25675
  transactions;
@@ -25661,7 +25723,7 @@ var PostgresSession = class PostgresSession {
25661
25723
  if (this.#broken !== null) throw new PluginError("connectionLost", `The connection was lost: ${this.#broken.message}`);
25662
25724
  if (this.#running !== null) throw new PluginError("sessionBusy", BUSY_MESSAGE);
25663
25725
  await this.#cancelling.catch(() => void 0);
25664
- const running = { cancelRequested: false };
25726
+ const running = startRunning();
25665
25727
  this.#running = running;
25666
25728
  const effect = transactionEffect(sql, DIALECT);
25667
25729
  const state = this.#transaction;
@@ -25670,6 +25732,10 @@ var PostgresSession = class PostgresSession {
25670
25732
  await this.#client.query(BEGIN_SQL);
25671
25733
  state.open = true;
25672
25734
  }
25735
+ running.limitMs = await this.#applyTimeout(options.serverTimeoutMs);
25736
+ if (namesServerTimeout(sql)) this.#appliedTimeoutMs = null;
25737
+ if (running.cancelRequested) throw new Error(CANCELLED_MESSAGE);
25738
+ running.startedAt = Date.now();
25673
25739
  } catch (cause) {
25674
25740
  this.#running = null;
25675
25741
  throw this.#failure(cause, running);
@@ -25682,10 +25748,11 @@ var PostgresSession = class PostgresSession {
25682
25748
  }));
25683
25749
  try {
25684
25750
  const first = await readBatch(cursor).catch((cause) => {
25685
- if (effect === "end") state.open = false;
25751
+ if (effect === "end") this.#transactionEnded();
25686
25752
  throw cause;
25687
25753
  });
25688
- if (effect !== null) state.open = effect === "begin";
25754
+ if (effect === "end") this.#transactionEnded();
25755
+ else if (effect !== null) state.open = true;
25689
25756
  const fields = first.result.fields;
25690
25757
  if (fields.length === 0) {
25691
25758
  this.#notices = null;
@@ -25740,7 +25807,7 @@ var PostgresSession = class PostgresSession {
25740
25807
  if (this.#running !== null) throw new PluginError("sessionBusy", BUSY_MESSAGE);
25741
25808
  if (rows.length === 0) return;
25742
25809
  await this.#cancelling.catch(() => void 0);
25743
- const running = { cancelRequested: false };
25810
+ const running = startRunning();
25744
25811
  this.#running = running;
25745
25812
  const state = this.#transaction;
25746
25813
  try {
@@ -25783,7 +25850,8 @@ var PostgresSession = class PostgresSession {
25783
25850
  if (!verdict.ok) throw new PluginError("explainUnsupported", verdict.reason);
25784
25851
  const statement = explainStatement(sql, options.analyze);
25785
25852
  const rows = [];
25786
- const result = await this.execute(statement);
25853
+ const limit = options.serverTimeoutMs;
25854
+ const result = await this.execute(statement, [], limit === void 0 ? {} : { serverTimeoutMs: limit });
25787
25855
  for await (const row of result.rows) rows.push(row);
25788
25856
  return {
25789
25857
  ...readPlan(planText(rows), statement, options.analyze),
@@ -25815,7 +25883,9 @@ var PostgresSession = class PostgresSession {
25815
25883
  }
25816
25884
  #failure(cause, running, sql) {
25817
25885
  const code = cause.code;
25818
- if (running.cancelRequested || code === QUERY_CANCELED) return new PluginError("cancelled", "The statement was cancelled.", { cause });
25886
+ const limit = running.limitMs;
25887
+ if (!running.cancelRequested && code === QUERY_CANCELED && limit !== null && Date.now() - running.startedAt >= limit) return new PluginError("timedOut", `PostgreSQL stopped the statement at its ${String(limit / MS_PER_SECOND)} s limit (statement_timeout).`, { cause });
25888
+ if (running.cancelRequested || code === QUERY_CANCELED) return new PluginError("cancelled", CANCELLED_MESSAGE, { cause });
25819
25889
  const position = sql === void 0 ? void 0 : serverErrorPosition(sql, cause);
25820
25890
  return new PluginError("statementFailed", messageOf(cause), {
25821
25891
  cause,
@@ -25852,7 +25922,37 @@ var PostgresSession = class PostgresSession {
25852
25922
  } catch (cause) {
25853
25923
  throw new PluginError("statementFailed", messageOf(cause), { cause });
25854
25924
  } finally {
25855
- this.#transaction.open = false;
25925
+ this.#transactionEnded();
25926
+ }
25927
+ }
25928
+ /** `statement_timeout` is transactional: a `ROLLBACK` undoes a `SET` made inside it. */
25929
+ #transactionEnded() {
25930
+ this.#transaction.open = false;
25931
+ if (this.#appliedTimeoutMs !== null) this.#appliedTimeoutMs = UNKNOWN_TIMEOUT;
25932
+ }
25933
+ /**
25934
+ * Sets `statement_timeout` to `requested`, never above the session's own, or puts the own back
25935
+ * when nothing is requested; sends nothing when the value would not change (decision 129). In a
25936
+ * failed transaction, which refuses the `SET`, it is skipped and the statement fails on its own.
25937
+ * Returns the limit the statement runs under, null for none.
25938
+ */
25939
+ async #applyTimeout(requested) {
25940
+ if (requested === void 0 && this.#appliedTimeoutMs === null) return null;
25941
+ try {
25942
+ if (this.#ownTimeoutMs === null) {
25943
+ const shown = await this.#client.query(READ_TIMEOUT_SQL);
25944
+ this.#ownTimeoutMs = postgresDurationMs(shown.rows[0]?.statement_timeout ?? "0");
25945
+ }
25946
+ const own = this.#ownTimeoutMs;
25947
+ const want = requested === void 0 ? own : own === 0 ? requested : Math.min(own, requested);
25948
+ if (want !== this.#appliedTimeoutMs) {
25949
+ await this.#client.query(SET_TIMEOUT_SQL, [String(want)]);
25950
+ this.#appliedTimeoutMs = want;
25951
+ }
25952
+ return want === 0 ? null : want;
25953
+ } catch (cause) {
25954
+ if (cause.code !== "25P02") throw cause;
25955
+ return null;
25856
25956
  }
25857
25957
  }
25858
25958
  async close() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablecraft-app/plugin-postgres",
3
- "version": "1.12.0",
3
+ "version": "1.13.0",
4
4
  "description": "The PostgreSQL datasource plugin for TableCraft, downloaded by the app when a data source needs it.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -24,6 +24,6 @@
24
24
  },
25
25
  "tableCraft": {
26
26
  "id": "postgres",
27
- "apiVersion": "1.5.0"
27
+ "apiVersion": "1.6.0"
28
28
  }
29
29
  }
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "id": "postgres",
3
3
  "name": "PostgreSQL",
4
- "version": "1.12.0",
5
- "apiVersion": "1.5.0",
4
+ "version": "1.13.0",
5
+ "apiVersion": "1.6.0",
6
6
  "entry": "index.js",
7
7
  "capabilities": [
8
8
  "sql",
@@ -18,9 +18,16 @@
18
18
  "objects",
19
19
  "serverMessages",
20
20
  "sessions",
21
- "routineCall"
21
+ "routineCall",
22
+ "serverTimeout"
22
23
  ],
23
24
  "highlight": "postgresql",
25
+ "brands": [
26
+ {
27
+ "id": "postgresql",
28
+ "name": "PostgreSQL"
29
+ }
30
+ ],
24
31
  "connection": {
25
32
  "fields": [
26
33
  {