@happyvertical/sql 0.89.0 → 0.89.1

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/README.md CHANGED
@@ -32,6 +32,8 @@ const tursoDb = await getDatabase({
32
32
  const pgDb = await getDatabase({
33
33
  type: 'postgres',
34
34
  url: 'postgresql://user:pass@localhost:5432/dbname',
35
+ connectionTimeoutMillis: 10_000,
36
+ idleTimeoutMillis: 30_000,
35
37
  });
36
38
 
37
39
  // DuckDB with JSON file auto-registration
@@ -50,6 +52,11 @@ const jsonDb = await getDatabase({
50
52
  });
51
53
  ```
52
54
 
55
+ PostgreSQL also accepts `max` (20 by default), `connectionTimeoutMillis`, and
56
+ `idleTimeoutMillis`. Lifecycle timeouts must be integer milliseconds from `0`
57
+ through Node.js's maximum timer delay of `2,147,483,647`; `0` disables the
58
+ corresponding timeout. Omitted timeout values retain `pg`'s defaults.
59
+
53
60
  ### Connection caching and cleanup
54
61
 
55
62
  PostgreSQL, JSON, and explicitly identified SQLite connections are shared by
@@ -82,10 +89,12 @@ always `await clearConnectionCache()` before opening a replacement connection.
82
89
  An explicit `dbid` is an opaque, stable caller-owned cache identity and must be
83
90
  non-empty. Without
84
91
  one, PostgreSQL derives a credential- and pool-option-sensitive identity using
85
- a process-keyed digest; connection URLs, usernames, passwords, and option names
86
- are not stored in readable cache keys. SQLite caches only connections with a
87
- `dbid` (automatically assigned to the default `:memory:` path). JSON derives an
88
- identity from its directory and behavior options.
92
+ a process-keyed digest. The pool identity includes `max`,
93
+ `connectionTimeoutMillis`, and `idleTimeoutMillis`; connection URLs, usernames,
94
+ passwords, and option names are not stored in readable cache keys. SQLite
95
+ caches only connections with a `dbid` (automatically assigned to the default
96
+ `:memory:` path). JSON derives an identity from its directory and behavior
97
+ options.
89
98
 
90
99
  DuckDB already creates a fresh adapter for every call, so `cache` and
91
100
  `clearCache` are accepted for uniform configuration but do not change its
@@ -364,6 +373,10 @@ await syncSchema({
364
373
  const exists = await db.tableExists('users');
365
374
  ```
366
375
 
376
+ On PostgreSQL, `syncSchema()` recognizes `CREATE [UNIQUE] INDEX CONCURRENTLY
377
+ [IF NOT EXISTS]` statements, including optional `USING` index methods, and
378
+ skips indexes that already exist when a schema is applied again.
379
+
367
380
  ### Vector Search (PostgreSQL)
368
381
 
369
382
  PostgreSQL adapters expose `db.vector` when pgvector is available:
package/dist/index.js CHANGED
@@ -1717,6 +1717,7 @@ var postgres_exports = /* @__PURE__ */ __exportAll({
1717
1717
  clearPostgresConnectionCache: () => clearPostgresConnectionCache,
1718
1718
  getDatabase: () => getDatabase$1
1719
1719
  });
1720
+ var MAX_TIMER_DELAY_MILLIS = 2147483647;
1720
1721
  globalThis.__haveSqlPostgresCacheHmacKey ??= webcrypto.subtle.generateKey({
1721
1722
  name: "HMAC",
1722
1723
  hash: "SHA-256"
@@ -1753,8 +1754,11 @@ function resolvePostgresConfig(options) {
1753
1754
  const legacyPort = process.env.SQLOO_PORT;
1754
1755
  const port = legacy(primary.port, legacyPort === void 0 ? void 0 : Number(legacyPort)) ?? 5432;
1755
1756
  const max = options.max ?? 20;
1757
+ const { connectionTimeoutMillis, idleTimeoutMillis } = options;
1756
1758
  if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error("PostgreSQL port must be an integer between 1 and 65535");
1757
1759
  if (!Number.isInteger(max) || max < 1) throw new Error("PostgreSQL pool max must be a positive integer");
1760
+ if (connectionTimeoutMillis !== void 0 && (!Number.isInteger(connectionTimeoutMillis) || connectionTimeoutMillis < 0 || connectionTimeoutMillis > MAX_TIMER_DELAY_MILLIS)) throw new Error(`PostgreSQL pool connectionTimeoutMillis must be an integer between 0 and ${MAX_TIMER_DELAY_MILLIS}`);
1761
+ if (idleTimeoutMillis !== void 0 && (!Number.isInteger(idleTimeoutMillis) || idleTimeoutMillis < 0 || idleTimeoutMillis > MAX_TIMER_DELAY_MILLIS)) throw new Error(`PostgreSQL pool idleTimeoutMillis must be an integer between 0 and ${MAX_TIMER_DELAY_MILLIS}`);
1758
1762
  return {
1759
1763
  url: legacy(primary.url, process.env.SQLOO_URL),
1760
1764
  database: legacy(primary.database, process.env.SQLOO_DATABASE),
@@ -1762,7 +1766,9 @@ function resolvePostgresConfig(options) {
1762
1766
  user: legacy(primary.user, process.env.SQLOO_USER),
1763
1767
  password: legacy(primary.password, process.env.SQLOO_PASSWORD),
1764
1768
  port,
1765
- max
1769
+ max,
1770
+ connectionTimeoutMillis,
1771
+ idleTimeoutMillis
1766
1772
  };
1767
1773
  }
1768
1774
  async function derivePostgresConnectionCacheKey(options, effective) {
@@ -1774,7 +1780,9 @@ async function derivePostgresConnectionCacheKey(options, effective) {
1774
1780
  database: effective.database ?? null,
1775
1781
  user: effective.user ?? null,
1776
1782
  password: effective.password ?? null,
1777
- max: effective.max
1783
+ max: effective.max,
1784
+ connectionTimeoutMillis: effective.connectionTimeoutMillis ?? null,
1785
+ idleTimeoutMillis: effective.idleTimeoutMillis ?? null
1778
1786
  });
1779
1787
  const hmacKey = await globalThis.__haveSqlPostgresCacheHmacKey;
1780
1788
  if (!hmacKey) throw new Error("PostgreSQL cache identity key was not initialized");
@@ -2115,17 +2123,21 @@ async function getDatabase$1(options = {}) {
2115
2123
  async function createDatabase(config) {
2116
2124
  const { database, host = "localhost", user, password, port = 5432 } = config;
2117
2125
  const url = config.url || `postgresql://${user}${password ? `:${password}` : ""}@${host}:${port}/${database || "postgres"}`;
2118
- const poolMax = config.max;
2126
+ const { max: poolMax, connectionTimeoutMillis, idleTimeoutMillis } = config;
2119
2127
  const pool = new Pool(config.url ? {
2120
2128
  connectionString: config.url,
2121
- max: poolMax
2129
+ max: poolMax,
2130
+ connectionTimeoutMillis,
2131
+ idleTimeoutMillis
2122
2132
  } : {
2123
2133
  host,
2124
2134
  user,
2125
2135
  password,
2126
2136
  port,
2127
2137
  database,
2128
- max: poolMax
2138
+ max: poolMax,
2139
+ connectionTimeoutMillis,
2140
+ idleTimeoutMillis
2129
2141
  });
2130
2142
  pool.on("error", (error) => {
2131
2143
  console.warn("Warning: PostgreSQL pool client error (connection discarded):", error instanceof Error ? error.message : String(error));
@@ -2804,11 +2816,18 @@ async function createDatabase(config) {
2804
2816
  };
2805
2817
  const syncSchema = async (schema) => {
2806
2818
  const commands = schema.trim().split(";").filter((command) => command.trim() !== "");
2807
- const createIndexRegex = /CREATE (UNIQUE )?INDEX (IF NOT EXISTS )?"?(\w+)"? ON "?(\w+)"?\s*\(([^)]+)\)/i;
2819
+ const createIndexRegex = /CREATE\s+(?:UNIQUE\s+)?INDEX\s+(?:CONCURRENTLY\s+)?(?:IF\s+NOT\s+EXISTS\s+)?"?(?<indexName>\w+)"?\s+ON\s+"?(?<tableName>\w+)"?\s*(?:USING\s+"?\w+"?\s*)?\([^)]+\)/i;
2820
+ const parseCreateIndex = (command) => {
2821
+ const { indexName, tableName } = command.match(createIndexRegex)?.groups ?? {};
2822
+ return indexName && tableName ? {
2823
+ indexName,
2824
+ tableName
2825
+ } : null;
2826
+ };
2808
2827
  const indexNames = [];
2809
2828
  for (const command of commands) {
2810
- const indexMatch = command.trim().match(createIndexRegex);
2811
- if (indexMatch) indexNames.push(indexMatch[3]);
2829
+ const indexMatch = parseCreateIndex(command.trim());
2830
+ if (indexMatch) indexNames.push(indexMatch.indexName);
2812
2831
  }
2813
2832
  const existingIndexes = /* @__PURE__ */ new Set();
2814
2833
  if (indexNames.length > 0) {
@@ -2850,10 +2869,9 @@ async function createDatabase(config) {
2850
2869
  }
2851
2870
  continue;
2852
2871
  }
2853
- const indexMatch = trimmedCommand.match(createIndexRegex);
2872
+ const indexMatch = parseCreateIndex(trimmedCommand);
2854
2873
  if (indexMatch) {
2855
- const indexName = indexMatch[3];
2856
- const indexTableName = indexMatch[4];
2874
+ const { indexName, tableName: indexTableName } = indexMatch;
2857
2875
  if (!existingIndexes.has(indexName)) try {
2858
2876
  await runTolerated(() => executor.query(trimmedCommand));
2859
2877
  existingIndexes.add(indexName);