turbine-orm 0.5.0 → 0.7.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.
Files changed (46) hide show
  1. package/README.md +194 -26
  2. package/dist/cjs/cli/config.js +5 -15
  3. package/dist/cjs/cli/index.js +240 -41
  4. package/dist/cjs/cli/migrate.js +71 -46
  5. package/dist/cjs/cli/ui.js +5 -9
  6. package/dist/cjs/client.js +109 -46
  7. package/dist/cjs/errors.js +293 -0
  8. package/dist/cjs/generate.js +33 -13
  9. package/dist/cjs/index.js +39 -20
  10. package/dist/cjs/introspect.js +3 -5
  11. package/dist/cjs/pipeline.js +9 -2
  12. package/dist/cjs/query.js +442 -109
  13. package/dist/cjs/schema-builder.js +93 -24
  14. package/dist/cjs/schema-sql.js +157 -19
  15. package/dist/cjs/schema.js +5 -2
  16. package/dist/cjs/serverless.js +87 -176
  17. package/dist/cli/config.js +6 -16
  18. package/dist/cli/index.js +245 -46
  19. package/dist/cli/migrate.d.ts +6 -1
  20. package/dist/cli/migrate.js +72 -47
  21. package/dist/cli/ui.js +5 -9
  22. package/dist/client.d.ts +77 -4
  23. package/dist/client.js +109 -46
  24. package/dist/errors.d.ts +138 -0
  25. package/dist/errors.js +278 -0
  26. package/dist/generate.d.ts +1 -1
  27. package/dist/generate.js +36 -16
  28. package/dist/index.d.ts +11 -9
  29. package/dist/index.js +16 -12
  30. package/dist/introspect.d.ts +1 -1
  31. package/dist/introspect.js +4 -6
  32. package/dist/pipeline.d.ts +1 -1
  33. package/dist/pipeline.js +9 -2
  34. package/dist/query.d.ts +257 -36
  35. package/dist/query.js +443 -110
  36. package/dist/schema-builder.d.ts +2 -2
  37. package/dist/schema-builder.js +93 -25
  38. package/dist/schema-sql.d.ts +7 -3
  39. package/dist/schema-sql.js +157 -19
  40. package/dist/schema.d.ts +1 -1
  41. package/dist/schema.js +5 -2
  42. package/dist/serverless.d.ts +91 -139
  43. package/dist/serverless.js +86 -173
  44. package/package.json +33 -16
  45. package/dist/types.d.ts +0 -93
  46. package/dist/types.js +0 -126
@@ -27,15 +27,19 @@ exports.createMigration = createMigration;
27
27
  exports.migrateUp = migrateUp;
28
28
  exports.migrateDown = migrateDown;
29
29
  exports.migrateStatus = migrateStatus;
30
+ const node_crypto_1 = require("node:crypto");
30
31
  const node_fs_1 = require("node:fs");
31
32
  const node_path_1 = require("node:path");
32
33
  const pg_1 = __importDefault(require("pg"));
34
+ const errors_js_1 = require("../errors.js");
35
+ const query_js_1 = require("../query.js");
33
36
  // ---------------------------------------------------------------------------
34
37
  // Tracking table management
35
38
  // ---------------------------------------------------------------------------
36
39
  const TRACKING_TABLE = '_turbine_migrations';
40
+ const QUOTED_TRACKING_TABLE = (0, query_js_1.quoteIdent)(TRACKING_TABLE);
37
41
  const CREATE_TRACKING_TABLE = `
38
- CREATE TABLE IF NOT EXISTS ${TRACKING_TABLE} (
42
+ CREATE TABLE IF NOT EXISTS ${QUOTED_TRACKING_TABLE} (
39
43
  id SERIAL PRIMARY KEY,
40
44
  name TEXT NOT NULL UNIQUE,
41
45
  checksum TEXT NOT NULL,
@@ -47,7 +51,7 @@ async function ensureTrackingTable(client) {
47
51
  }
48
52
  async function getAppliedMigrations(client) {
49
53
  await ensureTrackingTable(client);
50
- const result = await client.query(`SELECT id, name, applied_at, checksum FROM ${TRACKING_TABLE} ORDER BY id ASC`);
54
+ const result = await client.query(`SELECT id, name, applied_at, checksum FROM ${QUOTED_TRACKING_TABLE} ORDER BY id ASC`);
51
55
  return result.rows;
52
56
  }
53
57
  // ---------------------------------------------------------------------------
@@ -155,30 +159,45 @@ function parseMigrationSQL(filePath) {
155
159
  return parseMigrationContent(content);
156
160
  }
157
161
  /**
158
- * Simple checksum for a migration file (for drift detection).
162
+ * SHA-256 checksum for migration drift detection.
163
+ * Returns a hex-encoded hash of the file content.
159
164
  */
160
165
  function checksum(content) {
161
- let hash = 0;
162
- for (let i = 0; i < content.length; i++) {
163
- const chr = content.charCodeAt(i);
164
- hash = ((hash << 5) - hash + chr) | 0;
165
- }
166
- return Math.abs(hash).toString(36);
166
+ return (0, node_crypto_1.createHash)('sha256').update(content, 'utf-8').digest('hex');
167
+ }
168
+ /** Detect legacy djb2 checksums (short alphanumeric strings, pre-v0.6) */
169
+ function isLegacyChecksum(hash) {
170
+ return hash.length < 64;
167
171
  }
168
172
  // ---------------------------------------------------------------------------
169
173
  // Commands
170
174
  // ---------------------------------------------------------------------------
171
175
  /**
172
176
  * Create a new migration file.
177
+ * If `autoContent` is provided, the UP/DOWN sections are pre-populated with the given SQL.
173
178
  */
174
- function createMigration(migrationsDir, name) {
179
+ function createMigration(migrationsDir, name, autoContent) {
175
180
  (0, node_fs_1.mkdirSync)(migrationsDir, { recursive: true });
176
181
  const now = new Date();
177
182
  const ts = formatTimestamp(now);
178
183
  const safeName = sanitizeName(name);
179
184
  const filename = `${ts}_${safeName}.sql`;
180
185
  const filePath = (0, node_path_1.join)(migrationsDir, filename);
181
- const template = `-- Migration: ${name}
186
+ let template;
187
+ if (autoContent) {
188
+ template = `-- Migration: ${name} (auto-generated from schema diff)
189
+ -- Created: ${now.toISOString()}
190
+ -- Review this file before running: npx turbine migrate up
191
+
192
+ -- UP
193
+ ${autoContent.up}
194
+
195
+ -- DOWN
196
+ ${autoContent.down}
197
+ `;
198
+ }
199
+ else {
200
+ template = `-- Migration: ${name}
182
201
  -- Created: ${now.toISOString()}
183
202
 
184
203
  -- UP
@@ -187,6 +206,7 @@ function createMigration(migrationsDir, name) {
187
206
  -- DOWN
188
207
  -- Write your rollback SQL here
189
208
  `;
209
+ }
190
210
  (0, node_fs_1.writeFileSync)(filePath, template, 'utf-8');
191
211
  return {
192
212
  filename,
@@ -201,17 +221,16 @@ function createMigration(migrationsDir, name) {
201
221
  /** Fixed lock ID for Turbine migrations — prevents concurrent migrate runs */
202
222
  const MIGRATION_LOCK_ID = 8_347_291; // arbitrary but stable
203
223
  async function acquireLock(client) {
204
- const result = await client.query(`SELECT pg_try_advisory_lock($1)`, [MIGRATION_LOCK_ID]);
224
+ const result = await client.query(`SELECT pg_try_advisory_lock($1)`, [
225
+ MIGRATION_LOCK_ID,
226
+ ]);
205
227
  return result.rows[0]?.pg_try_advisory_lock ?? false;
206
228
  }
207
229
  async function releaseLock(client) {
208
230
  await client.query(`SELECT pg_advisory_unlock($1)`, [MIGRATION_LOCK_ID]);
209
231
  }
210
- // ---------------------------------------------------------------------------
211
- // Checksum validation
212
- // ---------------------------------------------------------------------------
213
232
  /**
214
- * Validate that applied migration files have not been modified since they were run.
233
+ * Validate that applied migration files have not been modified or deleted since they were run.
215
234
  * Returns an array of mismatched migrations (empty if all are clean).
216
235
  */
217
236
  async function validateChecksums(client, migrationsDir) {
@@ -221,15 +240,31 @@ async function validateChecksums(client, migrationsDir) {
221
240
  const mismatches = [];
222
241
  for (const migration of applied) {
223
242
  const file = fileMap.get(migration.name);
224
- if (!file)
225
- continue; // file deleted — not a checksum issue
243
+ if (!file) {
244
+ mismatches.push({
245
+ name: migration.name,
246
+ expected: migration.checksum,
247
+ actual: '',
248
+ type: 'missing',
249
+ });
250
+ continue;
251
+ }
226
252
  const content = (0, node_fs_1.readFileSync)(file.path, 'utf-8');
227
253
  const currentHash = checksum(content);
228
254
  if (currentHash !== migration.checksum) {
255
+ // Auto-upgrade legacy djb2 checksums to SHA-256 without flagging as modified
256
+ if (isLegacyChecksum(migration.checksum)) {
257
+ await client.query(`UPDATE ${QUOTED_TRACKING_TABLE} SET checksum = $1 WHERE name = $2`, [
258
+ currentHash,
259
+ migration.name,
260
+ ]);
261
+ continue;
262
+ }
229
263
  mismatches.push({
230
264
  name: migration.name,
231
265
  expected: migration.checksum,
232
266
  actual: currentHash,
267
+ type: 'modified',
233
268
  });
234
269
  }
235
270
  }
@@ -251,27 +286,23 @@ async function migrateUp(connectionString, migrationsDir, options) {
251
286
  // Acquire advisory lock to prevent concurrent migrations
252
287
  const gotLock = await acquireLock(client);
253
288
  if (!gotLock) {
254
- return {
255
- applied: [],
256
- errors: [{
257
- file: { filename: '', path: '', name: '', timestamp: '' },
258
- error: 'Could not acquire migration lock — another migration is already running',
259
- }],
260
- };
289
+ throw new errors_js_1.MigrationError('[turbine] Could not acquire migration lock — another migration is already running');
261
290
  }
262
291
  try {
263
292
  await ensureTrackingTable(client);
264
- // Validate checksums of already-applied migrations
265
- const mismatches = await validateChecksums(client, migrationsDir);
266
- if (mismatches.length > 0) {
267
- const names = mismatches.map((m) => m.name).join(', ');
268
- return {
269
- applied: [],
270
- errors: [{
271
- file: { filename: '', path: '', name: '', timestamp: '' },
272
- error: `Checksum mismatch: migration file(s) modified after application: ${names}. This is dangerous — applied migrations should be immutable. Use --force to skip this check.`,
273
- }],
274
- };
293
+ // Validate checksums of already-applied migrations (skip with --force)
294
+ if (!options?.force) {
295
+ const mismatches = await validateChecksums(client, migrationsDir);
296
+ if (mismatches.length > 0) {
297
+ const modified = mismatches.filter((m) => m.type === 'modified');
298
+ const missing = mismatches.filter((m) => m.type === 'missing');
299
+ const parts = [];
300
+ if (modified.length > 0)
301
+ parts.push(`modified: ${modified.map((m) => m.name).join(', ')}`);
302
+ if (missing.length > 0)
303
+ parts.push(`deleted: ${missing.map((m) => m.name).join(', ')}`);
304
+ throw new errors_js_1.MigrationError(`[turbine] Migration integrity check failed — ${parts.join('; ')}. Applied migrations should be immutable. Use --force to skip this check.`);
305
+ }
275
306
  }
276
307
  const applied = await getAppliedMigrations(client);
277
308
  const appliedNames = new Set(applied.map((m) => m.name));
@@ -293,7 +324,7 @@ async function migrateUp(connectionString, migrationsDir, options) {
293
324
  try {
294
325
  await client.query('BEGIN');
295
326
  await client.query(up);
296
- await client.query(`INSERT INTO ${TRACKING_TABLE} (name, checksum) VALUES ($1, $2) ON CONFLICT (name) DO NOTHING`, [file.name, hash]);
327
+ await client.query(`INSERT INTO ${QUOTED_TRACKING_TABLE} (name, checksum) VALUES ($1, $2) ON CONFLICT (name) DO NOTHING`, [file.name, hash]);
297
328
  await client.query('COMMIT');
298
329
  results.push(file);
299
330
  }
@@ -329,13 +360,7 @@ async function migrateDown(connectionString, migrationsDir, options) {
329
360
  try {
330
361
  const gotLock = await acquireLock(client);
331
362
  if (!gotLock) {
332
- return {
333
- rolledBack: [],
334
- errors: [{
335
- file: { filename: '', path: '', name: '', timestamp: '' },
336
- error: 'Could not acquire migration lock — another migration is already running',
337
- }],
338
- };
363
+ throw new errors_js_1.MigrationError('[turbine] Could not acquire migration lock — another migration is already running');
339
364
  }
340
365
  try {
341
366
  await ensureTrackingTable(client);
@@ -353,7 +378,7 @@ async function migrateDown(connectionString, migrationsDir, options) {
353
378
  const file = fileMap.get(migration.name);
354
379
  if (!file) {
355
380
  errors.push({
356
- file: { filename: migration.name + '.sql', path: '', name: migration.name, timestamp: '' },
381
+ file: { filename: `${migration.name}.sql`, path: '', name: migration.name, timestamp: '' },
357
382
  error: `Migration file not found for "${migration.name}"`,
358
383
  });
359
384
  continue;
@@ -366,7 +391,7 @@ async function migrateDown(connectionString, migrationsDir, options) {
366
391
  try {
367
392
  await client.query('BEGIN');
368
393
  await client.query(down);
369
- await client.query(`DELETE FROM ${TRACKING_TABLE} WHERE name = $1`, [migration.name]);
394
+ await client.query(`DELETE FROM ${QUOTED_TRACKING_TABLE} WHERE name = $1`, [migration.name]);
370
395
  await client.query('COMMIT');
371
396
  results.push(file);
372
397
  }
@@ -24,9 +24,7 @@ exports.redactUrl = redactUrl;
24
24
  // ---------------------------------------------------------------------------
25
25
  // ANSI escape codes
26
26
  // ---------------------------------------------------------------------------
27
- const isColorSupported = process.env['NO_COLOR'] == null &&
28
- process.env['TERM'] !== 'dumb' &&
29
- (process.stdout.isTTY ?? false);
27
+ const isColorSupported = process.env.NO_COLOR == null && process.env.TERM !== 'dumb' && (process.stdout.isTTY ?? false);
30
28
  function code(open, close) {
31
29
  if (!isColorSupported)
32
30
  return (s) => s;
@@ -113,9 +111,7 @@ function table(headers, rows) {
113
111
  return ` ${(0, exports.bold)(h)}${' '.repeat(Math.max(0, w - stripAnsi(h).length))} `;
114
112
  })
115
113
  .join((0, exports.dim)(exports.symbols.vertLine));
116
- const separator = colWidths
117
- .map((w) => exports.symbols.line.repeat(w + 2))
118
- .join((0, exports.dim)(exports.symbols.line));
114
+ const separator = colWidths.map((w) => exports.symbols.line.repeat(w + 2)).join((0, exports.dim)(exports.symbols.line));
119
115
  const bodyLines = rows.map((row) => row
120
116
  .map((cell, i) => {
121
117
  const w = colWidths[i];
@@ -195,7 +191,7 @@ function info(msg) {
195
191
  console.log(` ${(0, exports.blue)(exports.symbols.info)} ${msg}`);
196
192
  }
197
193
  function label(key, value) {
198
- console.log(` ${(0, exports.dim)(key + ':')} ${value}`);
194
+ console.log(` ${(0, exports.dim)(`${key}:`)} ${value}`);
199
195
  }
200
196
  function newline() {
201
197
  console.log('');
@@ -209,7 +205,7 @@ function divider() {
209
205
  // ---------------------------------------------------------------------------
210
206
  function banner() {
211
207
  console.log('');
212
- console.log(` ${(0, exports.bold)((0, exports.cyan)('turbine'))} ${(0, exports.dim)('by')} ${(0, exports.bold)('BataData')}`);
208
+ console.log(` ${(0, exports.bold)((0, exports.cyan)('turbine-orm'))}`);
213
209
  console.log(` ${(0, exports.dim)('TypeScript ORM with json_agg nested queries')}`);
214
210
  console.log('');
215
211
  }
@@ -226,7 +222,7 @@ function elapsed(startMs) {
226
222
  // Strip ANSI codes (for width calculation)
227
223
  // ---------------------------------------------------------------------------
228
224
  function stripAnsi(s) {
229
- // eslint-disable-next-line no-control-regex
225
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: ANSI escape codes require control characters
230
226
  return s.replace(/\x1b\[[0-9;]*m/g, '');
231
227
  }
232
228
  // ---------------------------------------------------------------------------
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  /**
3
- * @batadata/turbine — TurbineClient
3
+ * turbine-orm — TurbineClient
4
4
  *
5
5
  * The main entry point for the Turbine TypeScript SDK.
6
6
  * Manages connection pooling and provides typed table accessors.
@@ -17,7 +17,7 @@
17
17
  * const user = await db.users.findUnique({ where: { id: 1 } });
18
18
  *
19
19
  * // With base client (dynamic):
20
- * import { TurbineClient } from '@batadata/turbine';
20
+ * import { TurbineClient } from 'turbine-orm';
21
21
  * const db = new TurbineClient({ connectionString: '...' }, schema);
22
22
  * const users = db.table<User>('users');
23
23
  * ```
@@ -28,22 +28,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
28
28
  Object.defineProperty(exports, "__esModule", { value: true });
29
29
  exports.TurbineClient = exports.TransactionClient = void 0;
30
30
  const pg_1 = __importDefault(require("pg"));
31
- const query_js_1 = require("./query.js");
31
+ const errors_js_1 = require("./errors.js");
32
32
  const pipeline_js_1 = require("./pipeline.js");
33
- /**
34
- * Parse int8 (bigint, OID 20) as JavaScript number instead of string.
35
- * Safe for values up to Number.MAX_SAFE_INTEGER (9,007,199,254,740,991).
36
- *
37
- * NOTE: For values exceeding Number.MAX_SAFE_INTEGER, the parser falls back
38
- * to returning the raw string to avoid precision loss. The generated TypeScript
39
- * type maps int8/bigint to `number`, which is correct for the vast majority of
40
- * use cases (IDs, counts, timestamps). If you store values > 2^53 - 1 in a
41
- * bigint column, the runtime return type will be `string` for those rows.
42
- */
43
- pg_1.default.types.setTypeParser(20, (val) => {
44
- const n = Number(val);
45
- return Number.isSafeInteger(n) ? n : val; // fall back to string for huge values
46
- });
33
+ const query_js_1 = require("./query.js");
47
34
  /** Maps isolation level names to SQL */
48
35
  const ISOLATION_LEVELS = {
49
36
  ReadUncommitted: 'READ UNCOMMITTED',
@@ -125,20 +112,36 @@ class TransactionClient {
125
112
  sql += `$${i + 1}`;
126
113
  }
127
114
  });
128
- const result = await this.client.query(sql, values);
129
- return result.rows;
115
+ try {
116
+ const result = await this.client.query(sql, values);
117
+ return result.rows;
118
+ }
119
+ catch (err) {
120
+ throw (0, errors_js_1.wrapPgError)(err);
121
+ }
130
122
  }
131
123
  /**
132
124
  * Create a pool-like wrapper around the transaction client.
133
125
  * This allows QueryInterface to work with the transaction connection
134
126
  * without knowing it's in a transaction.
127
+ *
128
+ * pg driver errors thrown by queries are translated into typed Turbine
129
+ * errors via wrapPgError so transaction-scoped queries surface the same
130
+ * typed errors as pool-scoped queries.
135
131
  */
136
132
  createTxPool() {
137
133
  const client = this.client;
138
134
  // Return a minimal pool-compatible object that routes queries
139
135
  // through the transaction client
140
136
  return {
141
- query: (text, values) => client.query(text, values),
137
+ query: async (text, values) => {
138
+ try {
139
+ return await client.query(text, values);
140
+ }
141
+ catch (err) {
142
+ throw (0, errors_js_1.wrapPgError)(err);
143
+ }
144
+ },
142
145
  connect: () => Promise.resolve(client),
143
146
  };
144
147
  }
@@ -152,38 +155,81 @@ class TurbineClient {
152
155
  pool;
153
156
  /** The schema metadata this client was built from */
154
157
  schema;
158
+ static int8ParserRegistered = false;
155
159
  logging;
156
160
  tableCache = new Map();
157
161
  middlewares = [];
158
162
  queryOptions;
163
+ /** True when Turbine created the pool and is responsible for tearing it down */
164
+ ownsPool = true;
159
165
  constructor(config = {}, schema) {
166
+ /**
167
+ * Parse int8 (bigint, OID 20) as JavaScript number instead of string.
168
+ * Safe for values up to Number.MAX_SAFE_INTEGER (9,007,199,254,740,991).
169
+ *
170
+ * NOTE: For values exceeding Number.MAX_SAFE_INTEGER, the parser falls back
171
+ * to returning the raw string to avoid precision loss. The generated TypeScript
172
+ * type maps int8/bigint to `number`, which is correct for the vast majority of
173
+ * use cases (IDs, counts, timestamps). If you store values > 2^53 - 1 in a
174
+ * bigint column, the runtime return type will be `string` for those rows.
175
+ *
176
+ * NOTE: We intentionally do NOT register a parser for numeric (OID 1700).
177
+ * Postgres numeric is arbitrary-precision, so the default pg driver behavior
178
+ * of returning a string is correct and matches the generated TypeScript type
179
+ * (numeric → string). Users who want number can cast explicitly in SQL.
180
+ */
181
+ // Only register the int8 parser when we own the pg driver. External
182
+ // pools (Neon HTTP, Vercel Postgres) may ship their own pg-types fork
183
+ // and rely on their own parser configuration — don't mutate global state
184
+ // we don't own.
185
+ if (!config.pool && !TurbineClient.int8ParserRegistered) {
186
+ pg_1.default.types.setTypeParser(20, (val) => {
187
+ const n = Number(val);
188
+ return Number.isSafeInteger(n) ? n : val;
189
+ });
190
+ TurbineClient.int8ParserRegistered = true;
191
+ }
160
192
  this.logging = config.logging ?? false;
161
193
  this.schema = schema;
162
194
  this.queryOptions = {
163
195
  defaultLimit: config.defaultLimit,
164
196
  warnOnUnlimited: config.warnOnUnlimited,
165
197
  };
166
- const poolConfig = {
167
- max: config.poolSize ?? 10,
168
- idleTimeoutMillis: config.idleTimeoutMs ?? 30_000,
169
- connectionTimeoutMillis: config.connectionTimeoutMs ?? 5_000,
170
- };
171
- if (config.connectionString) {
172
- poolConfig.connectionString = config.connectionString;
198
+ if (config.pool) {
199
+ // External pool — use directly. Turbine doesn't manage its lifecycle.
200
+ this.pool = config.pool;
201
+ this.ownsPool = false;
202
+ if (this.logging) {
203
+ console.log(`[turbine] Using external pool — ${Object.keys(schema.tables).length} tables`);
204
+ }
173
205
  }
174
206
  else {
175
- poolConfig.host = config.host ?? 'localhost';
176
- poolConfig.port = config.port ?? 5432;
177
- poolConfig.database = config.database ?? 'postgres';
178
- poolConfig.user = config.user ?? 'postgres';
179
- poolConfig.password = config.password;
180
- }
181
- this.pool = new pg_1.default.Pool(poolConfig);
182
- this.pool.on('error', (err) => {
183
- console.error('[turbine] Unexpected pool error:', err.message);
184
- });
185
- if (this.logging) {
186
- console.log(`[turbine] Pool created — max ${poolConfig.max} connections, ${Object.keys(schema.tables).length} tables`);
207
+ const poolConfig = {
208
+ max: config.poolSize ?? 10,
209
+ idleTimeoutMillis: config.idleTimeoutMs ?? 30_000,
210
+ connectionTimeoutMillis: config.connectionTimeoutMs ?? 5_000,
211
+ };
212
+ if (config.connectionString) {
213
+ poolConfig.connectionString = config.connectionString;
214
+ }
215
+ else {
216
+ poolConfig.host = config.host ?? 'localhost';
217
+ poolConfig.port = config.port ?? 5432;
218
+ poolConfig.database = config.database ?? 'postgres';
219
+ poolConfig.user = config.user ?? 'postgres';
220
+ poolConfig.password = config.password;
221
+ }
222
+ if (config.ssl !== undefined) {
223
+ poolConfig.ssl = config.ssl;
224
+ }
225
+ this.pool = new pg_1.default.Pool(poolConfig);
226
+ this.ownsPool = true;
227
+ this.pool.on('error', (err) => {
228
+ console.error('[turbine] Unexpected pool error:', err.message);
229
+ });
230
+ if (this.logging) {
231
+ console.log(`[turbine] Pool created — max ${poolConfig.max} connections, ${Object.keys(schema.tables).length} tables`);
232
+ }
187
233
  }
188
234
  // Auto-create typed table accessors for all tables in the schema
189
235
  for (const tableName of Object.keys(schema.tables)) {
@@ -290,8 +336,13 @@ class TurbineClient {
290
336
  if (this.logging) {
291
337
  console.log(`[turbine] Raw SQL: ${sql.trim().substring(0, 120)}...`);
292
338
  }
293
- const result = await this.pool.query(sql, values);
294
- return result.rows;
339
+ try {
340
+ const result = await this.pool.query(sql, values);
341
+ return result.rows;
342
+ }
343
+ catch (err) {
344
+ throw (0, errors_js_1.wrapPgError)(err);
345
+ }
295
346
  }
296
347
  // -------------------------------------------------------------------------
297
348
  // Transaction support (raw — legacy)
@@ -375,7 +426,7 @@ class TurbineClient {
375
426
  let timer;
376
427
  const timeoutPromise = new Promise((_, reject) => {
377
428
  timer = setTimeout(() => {
378
- reject(new Error(`[turbine] Transaction timed out after ${timeout}ms`));
429
+ reject(new errors_js_1.TimeoutError(timeout, 'Transaction'));
379
430
  }, timeout);
380
431
  });
381
432
  try {
@@ -426,8 +477,17 @@ class TurbineClient {
426
477
  }
427
478
  /**
428
479
  * Gracefully shut down the connection pool.
480
+ *
481
+ * If Turbine was given an external pool via `TurbineConfig.pool`, this
482
+ * method is a no-op — the caller is responsible for the pool's lifecycle.
429
483
  */
430
484
  async disconnect() {
485
+ if (!this.ownsPool) {
486
+ if (this.logging) {
487
+ console.log('[turbine] disconnect() skipped — external pool is not owned by Turbine');
488
+ }
489
+ return;
490
+ }
431
491
  await this.pool.end();
432
492
  if (this.logging) {
433
493
  console.log('[turbine] Pool disconnected');
@@ -437,12 +497,15 @@ class TurbineClient {
437
497
  async end() {
438
498
  return this.disconnect();
439
499
  }
440
- /** Pool statistics for monitoring. */
500
+ /**
501
+ * Pool statistics for monitoring. Returns zeros for pools that don't
502
+ * expose connection counts (e.g., stateless HTTP drivers like Neon).
503
+ */
441
504
  get stats() {
442
505
  return {
443
- totalCount: this.pool.totalCount,
444
- idleCount: this.pool.idleCount,
445
- waitingCount: this.pool.waitingCount,
506
+ totalCount: this.pool.totalCount ?? 0,
507
+ idleCount: this.pool.idleCount ?? 0,
508
+ waitingCount: this.pool.waitingCount ?? 0,
446
509
  };
447
510
  }
448
511
  }