@rebasepro/server-postgres 0.25.0-canary.ge06257e → 0.25.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.
Files changed (126) hide show
  1. package/dist/{BranchService-DNg4txyf.js → BranchService-ClklxyAQ.js} +283 -20
  2. package/dist/BranchService-ClklxyAQ.js.map +1 -0
  3. package/dist/backup/backup-cli.d.ts +70 -1
  4. package/dist/backup/backup-service.d.ts +79 -0
  5. package/dist/backup/pg-binary.d.ts +2 -2
  6. package/dist/backup/pg-tools.d.ts +60 -1
  7. package/dist/backup-cli-gijeXe0u.js +1961 -0
  8. package/dist/backup-cli-gijeXe0u.js.map +1 -0
  9. package/dist/{boot-ddl-Dnvpmm_T.js → boot-ddl-DYH95Qeg.js} +3 -3
  10. package/dist/{boot-ddl-Dnvpmm_T.js.map → boot-ddl-DYH95Qeg.js.map} +1 -1
  11. package/dist/branch-argv.d.ts +15 -0
  12. package/dist/branch-prune.d.ts +18 -0
  13. package/dist/{cli-console-C_ISGwJo.js → cli-console-CQrQ0lAY.js} +19 -13
  14. package/dist/cli-console-CQrQ0lAY.js.map +1 -0
  15. package/dist/cli-console.d.ts +4 -3
  16. package/dist/{cli-errors-mx4sSjB8.js → cli-errors-DiA0XFIS.js} +110 -14
  17. package/dist/{cli-errors-mx4sSjB8.js.map → cli-errors-DiA0XFIS.js.map} +1 -1
  18. package/dist/cli-errors.d.ts +26 -1
  19. package/dist/cli-flags.d.ts +51 -0
  20. package/dist/{cli-prompt-egtNKkZl.js → cli-prompt-DXG0yk-h.js} +39 -4
  21. package/dist/{cli-prompt-egtNKkZl.js.map → cli-prompt-DXG0yk-h.js.map} +1 -1
  22. package/dist/cli.js +120 -184
  23. package/dist/cli.js.map +1 -1
  24. package/dist/collections/validate-relations.d.ts +8 -2
  25. package/dist/{column-plan-helpers-D_V4oaZh.js → column-plan-helpers-IG6uFb1s.js} +2 -2
  26. package/dist/{column-plan-helpers-D_V4oaZh.js.map → column-plan-helpers-IG6uFb1s.js.map} +1 -1
  27. package/dist/{connection-C938nR3E.js → connection-czvI7TZL.js} +57 -10
  28. package/dist/connection-czvI7TZL.js.map +1 -0
  29. package/dist/{databasePoolManager-Yqdw14wY.js → databasePoolManager-3stMNFL-.js} +4 -6
  30. package/dist/databasePoolManager-3stMNFL-.js.map +1 -0
  31. package/dist/db-command-line.d.ts +14 -0
  32. package/dist/{rls-bootstrap-sql-Bd-hJk08.js → db-subcommands-BPoHCxJR.js} +455 -3
  33. package/dist/db-subcommands-BPoHCxJR.js.map +1 -0
  34. package/dist/db-subcommands.d.ts +11 -0
  35. package/dist/{deferred-index-build-Ca6OqKyR.js → deferred-index-build-BE0I6sYU.js} +8 -8
  36. package/dist/{deferred-index-build-Ca6OqKyR.js.map → deferred-index-build-BE0I6sYU.js.map} +1 -1
  37. package/dist/{doctor-CJyuhV1n.js → doctor-BT--1kq3.js} +107 -11
  38. package/dist/doctor-BT--1kq3.js.map +1 -0
  39. package/dist/{ensure-collection-policies-C1ux3V_z.js → ensure-collection-policies-BnGOURpG.js} +60 -11
  40. package/dist/ensure-collection-policies-BnGOURpG.js.map +1 -0
  41. package/dist/{ensure-collection-tables-Do-Bvb64.js → ensure-collection-tables-Cet4650m.js} +9 -9
  42. package/dist/{ensure-collection-tables-Do-Bvb64.js.map → ensure-collection-tables-Cet4650m.js.map} +1 -1
  43. package/dist/{ensure-tables-Dl_Eh9ow.js → ensure-tables-l_OgFQjN.js} +5 -5
  44. package/dist/{ensure-tables-Dl_Eh9ow.js.map → ensure-tables-l_OgFQjN.js.map} +1 -1
  45. package/dist/{enum-labels-kept-D9eTIWCz.js → enum-labels-kept-nheku1pB.js} +3 -3
  46. package/dist/{enum-labels-kept-D9eTIWCz.js.map → enum-labels-kept-nheku1pB.js.map} +1 -1
  47. package/dist/{generate-drizzle-schema-Bf8canaR.js → generate-drizzle-schema-DC0BOUpn.js} +3 -3
  48. package/dist/{generate-drizzle-schema-Bf8canaR.js.map → generate-drizzle-schema-DC0BOUpn.js.map} +1 -1
  49. package/dist/{generate-drizzle-schema-logic-BaT0H7Gq.js → generate-drizzle-schema-logic-CPzCRD-8.js} +4 -4
  50. package/dist/{generate-drizzle-schema-logic-BaT0H7Gq.js.map → generate-drizzle-schema-logic-CPzCRD-8.js.map} +1 -1
  51. package/dist/{generate-files-D50Pxw1G.js → generate-files-BP-4W_gm.js} +6 -6
  52. package/dist/{generate-files-D50Pxw1G.js.map → generate-files-BP-4W_gm.js.map} +1 -1
  53. package/dist/{generate-next-step-DCd7yiJ2.js → generate-next-step-DgK5BlBn.js} +22 -14
  54. package/dist/{generate-next-step-DCd7yiJ2.js.map → generate-next-step-DgK5BlBn.js.map} +1 -1
  55. package/dist/{generate-postgres-ddl-logic-B4k9djAV.js → generate-postgres-ddl-logic-DPl1oH_z.js} +4 -4
  56. package/dist/{generate-postgres-ddl-logic-B4k9djAV.js.map → generate-postgres-ddl-logic-DPl1oH_z.js.map} +1 -1
  57. package/dist/generated-schema-diff-eZCklRZQ.js +1025 -0
  58. package/dist/generated-schema-diff-eZCklRZQ.js.map +1 -0
  59. package/dist/{generated-schema-staleness-FlxM2i4i.js → generated-schema-staleness-BvNF3i-s.js} +2 -2
  60. package/dist/{generated-schema-staleness-FlxM2i4i.js.map → generated-schema-staleness-BvNF3i-s.js.map} +1 -1
  61. package/dist/index.d.ts +1 -0
  62. package/dist/index.es.js +70 -11
  63. package/dist/index.es.js.map +1 -1
  64. package/dist/{introspect-db-logic-j2vYhmdP.js → introspect-db-logic-DgQVzccW.js} +2 -2
  65. package/dist/{introspect-db-logic-j2vYhmdP.js.map → introspect-db-logic-DgQVzccW.js.map} +1 -1
  66. package/dist/migrate-policy-stamps-3eqfhes6.js +89 -0
  67. package/dist/migrate-policy-stamps-3eqfhes6.js.map +1 -0
  68. package/dist/{open-rebase-D8rytTP0.js → open-rebase-EmqVmS14.js} +78 -1027
  69. package/dist/open-rebase-EmqVmS14.js.map +1 -0
  70. package/dist/{pg-error-utils-CdsGYMTm.js → pg-error-utils-CFEnm2m9.js} +11 -4
  71. package/dist/pg-error-utils-CFEnm2m9.js.map +1 -0
  72. package/dist/{plan-schema-CsNgK0qi.js → plan-schema-Czf3q4-Q.js} +4 -4
  73. package/dist/{plan-schema-CsNgK0qi.js.map → plan-schema-Czf3q4-Q.js.map} +1 -1
  74. package/dist/{policy-drift-CY5P1ZRu.js → policy-drift-X4A-xAzv.js} +3 -3
  75. package/dist/{policy-drift-CY5P1ZRu.js.map → policy-drift-X4A-xAzv.js.map} +1 -1
  76. package/dist/pool-disconnects.d.ts +28 -0
  77. package/dist/{push-index-prebuild-BDFcfRPo.js → push-index-prebuild-COd7i6wm.js} +4 -4
  78. package/dist/{push-index-prebuild-BDFcfRPo.js.map → push-index-prebuild-COd7i6wm.js.map} +1 -1
  79. package/dist/{push-legacy-renames-D4bvMWAb.js → push-legacy-renames-CLgYYwPE.js} +4 -4
  80. package/dist/{push-legacy-renames-D4bvMWAb.js.map → push-legacy-renames-CLgYYwPE.js.map} +1 -1
  81. package/dist/{push-policy-plan-DTXE6aVa.js → push-policy-plan-D2gpQsdv.js} +23 -7
  82. package/dist/push-policy-plan-D2gpQsdv.js.map +1 -0
  83. package/dist/{render-ddl-CrfEe__B.js → render-ddl-4v88y-Gv.js} +4 -4
  84. package/dist/{render-ddl-CrfEe__B.js.map → render-ddl-4v88y-Gv.js.map} +1 -1
  85. package/dist/{rls-enforcement-D7P4-ZYX.js → rls-enforcement-DDyNoOSy.js} +2 -2
  86. package/dist/{rls-enforcement-D7P4-ZYX.js.map → rls-enforcement-DDyNoOSy.js.map} +1 -1
  87. package/dist/schema/atlas-argv.d.ts +3 -1
  88. package/dist/schema/doctor-cli.js +5 -5
  89. package/dist/schema/doctor.d.ts +20 -2
  90. package/dist/schema/ensure-collection-policies.d.ts +28 -1
  91. package/dist/schema/generate-drizzle-schema.js +1 -1
  92. package/dist/schema/generate-next-step.d.ts +20 -8
  93. package/dist/schema/generate-postgres-ddl.js +2 -2
  94. package/dist/schema/generated-schema-diff.d.ts +21 -0
  95. package/dist/schema/introspect-db.js +6 -6
  96. package/dist/schema/introspect-db.js.map +1 -1
  97. package/dist/schema/migrate-policy-stamps.d.ts +23 -0
  98. package/dist/schema/push-policy-plan.d.ts +10 -2
  99. package/dist/services/BranchService.d.ts +85 -0
  100. package/dist/services/pg-notify-listener.d.ts +2 -0
  101. package/dist/{source-DB6xJv3p.js → source-fZb92pN8.js} +46 -3
  102. package/dist/source-fZb92pN8.js.map +1 -0
  103. package/dist/{vector-index-BFhyEaiz.js → vector-index-DsQ3V2d2.js} +2 -2
  104. package/dist/{vector-index-BFhyEaiz.js.map → vector-index-DsQ3V2d2.js.map} +1 -1
  105. package/dist/{websocket-DMeXOoyv.js → websocket-BKZwsjmf.js} +18 -5
  106. package/dist/websocket-BKZwsjmf.js.map +1 -0
  107. package/package.json +7 -7
  108. package/dist/BranchService-DNg4txyf.js.map +0 -1
  109. package/dist/backup-cli-HCEkPcQj.js +0 -556
  110. package/dist/backup-cli-HCEkPcQj.js.map +0 -1
  111. package/dist/backup-service-D-UpT7DH.js +0 -857
  112. package/dist/backup-service-D-UpT7DH.js.map +0 -1
  113. package/dist/cli-console-C_ISGwJo.js.map +0 -1
  114. package/dist/cli-flags-DUozBFyK.js +0 -205
  115. package/dist/cli-flags-DUozBFyK.js.map +0 -1
  116. package/dist/connection-C938nR3E.js.map +0 -1
  117. package/dist/databasePoolManager-Yqdw14wY.js.map +0 -1
  118. package/dist/doctor-CJyuhV1n.js.map +0 -1
  119. package/dist/ensure-collection-policies-C1ux3V_z.js.map +0 -1
  120. package/dist/open-rebase-D8rytTP0.js.map +0 -1
  121. package/dist/pg-error-utils-CdsGYMTm.js.map +0 -1
  122. package/dist/push-policy-plan-DTXE6aVa.js.map +0 -1
  123. package/dist/rls-bootstrap-sql-Bd-hJk08.js.map +0 -1
  124. package/dist/rolldown-runtime-V7rFbMRe.js +0 -39
  125. package/dist/source-DB6xJv3p.js.map +0 -1
  126. package/dist/websocket-DMeXOoyv.js.map +0 -1
@@ -1,30 +1,29 @@
1
1
  import { createRequire as __createRequire } from "module";
2
2
  __createRequire(import.meta.url);
3
- import { i as createPostgresDatabaseConnection } from "./connection-C938nR3E.js";
4
- import { _ as visibleColumnProjection, a as buildSearchColumnSpec, l as searchColumnNames, r as SEARCH_UNACCENT_FN, s as hiddenColumnsOption } from "./search-column-BM-GV6vH.js";
5
- import { i as diagnoseDbError, l as parseHostInfo, n as deepestErrorMessage, s as isEconnrefused, tt as extractCauseMessage } from "./cli-errors-mx4sSjB8.js";
6
- import { a as pgErrorToFriendlyMessage, i as isRowLevelSecurityDenial, n as extractPgError, o as reachedDatabase, r as isRoleSwitchingPermissionError, s as sanitizeErrorForClient, t as classifyConnectFailure } from "./pg-error-utils-CdsGYMTm.js";
7
- import { t as BranchService } from "./BranchService-DNg4txyf.js";
8
- import { a as ensureAppRoleGrants, d as warnOnLegacyRlsFunctions, f as warnOnRoleSchemaCollision, i as ensureAppRole, l as validatePolicyPgRoles, n as applyAuthContext, r as detectConnectionPosture, s as revokeUserRoleSql, t as REBASE_USER_ROLE$1, u as warnOnAnonymousGrants } from "./rls-enforcement-D7P4-ZYX.js";
9
- import { u as resolveColumnName } from "./column-plan-helpers-D_V4oaZh.js";
10
- import { i as isAuthCollection, t as AUTH_USERS_COLUMNS } from "./auth-users-columns-C72EMoDJ.js";
11
- import { n as planSchema, o as sharedRelationName } from "./plan-schema-CsNgK0qi.js";
12
- import { t as generateSchema } from "./generate-drizzle-schema-logic-BaT0H7Gq.js";
13
- import { r as renderPgType } from "./render-ddl-CrfEe__B.js";
14
- import { u as typesAgree } from "./deferred-index-build-Ca6OqKyR.js";
15
- import { a as isLockTimeout, c as resolveBootLockOptions, l as sleep, n as BOOT_LOCK_TIMEOUT_MS } from "./boot-ddl-Dnvpmm_T.js";
16
- import { a as readSchemaFactsFor, r as planCollectionSchemaEnsure } from "./ensure-collection-tables-Do-Bvb64.js";
17
- import { t as DatabasePoolManager } from "./databasePoolManager-Yqdw14wY.js";
18
- import { a as resolveAuthSchema, i as probeAuthSchema, r as assertAuthSchemaCompatible, t as ensureAuthTablesExist } from "./ensure-tables-Dl_Eh9ow.js";
19
- import { _ as humanize, g as mapPgType, i as buildTablesMap, l as getIconForTable, p as singularize, r as buildEnumMap, u as identifyJoinTables } from "./introspect-db-logic-j2vYhmdP.js";
3
+ import { i as createPostgresDatabaseConnection } from "./connection-czvI7TZL.js";
4
+ import { _ as visibleColumnProjection, a as buildSearchColumnSpec, r as SEARCH_UNACCENT_FN, s as hiddenColumnsOption } from "./search-column-BM-GV6vH.js";
5
+ import { a as diagnoseDbError, c as isEconnrefused, n as closedDatabaseName, nt as extractCauseMessage, r as deepestErrorMessage, u as parseHostInfo } from "./cli-errors-DiA0XFIS.js";
6
+ import { a as pgErrorToFriendlyMessage, i as isRowLevelSecurityDenial, n as extractPgError, o as reachedDatabase, r as isRoleSwitchingPermissionError, s as sanitizeErrorForClient, t as classifyConnectFailure } from "./pg-error-utils-CFEnm2m9.js";
7
+ import { t as BranchService } from "./BranchService-ClklxyAQ.js";
8
+ import { a as ensureAppRoleGrants, d as warnOnLegacyRlsFunctions, f as warnOnRoleSchemaCollision, i as ensureAppRole, l as validatePolicyPgRoles, n as applyAuthContext, r as detectConnectionPosture, s as revokeUserRoleSql, t as REBASE_USER_ROLE$1, u as warnOnAnonymousGrants } from "./rls-enforcement-DDyNoOSy.js";
9
+ import { u as resolveColumnName } from "./column-plan-helpers-IG6uFb1s.js";
10
+ import { n as planSchema } from "./plan-schema-Czf3q4-Q.js";
11
+ import { t as generateSchema } from "./generate-drizzle-schema-logic-CPzCRD-8.js";
12
+ import { r as renderPgType } from "./render-ddl-4v88y-Gv.js";
13
+ import { u as typesAgree } from "./deferred-index-build-BE0I6sYU.js";
14
+ import { a as isLockTimeout, c as resolveBootLockOptions, l as sleep, n as BOOT_LOCK_TIMEOUT_MS } from "./boot-ddl-DYH95Qeg.js";
15
+ import { a as readSchemaFactsFor, r as planCollectionSchemaEnsure } from "./ensure-collection-tables-Cet4650m.js";
16
+ import { t as DatabasePoolManager } from "./databasePoolManager-3stMNFL-.js";
17
+ import { a as resolveAuthSchema, i as probeAuthSchema, r as assertAuthSchemaCompatible, t as ensureAuthTablesExist } from "./ensure-tables-l_OgFQjN.js";
18
+ import { a as buildBaasSchema, c as introspectSchema, i as warnOnGeneratedSchemaDrift, l as readRlsStatus, n as diffGeneratedSchemaAgainstCatalogue, o as readCatalogueSchema, s as buildCollectionsFromSchema } from "./generated-schema-diff-eZCklRZQ.js";
20
19
  import { Client, DatabaseError, Pool } from "pg";
21
20
  import { drizzle } from "drizzle-orm/node-postgres";
22
21
  import { ApiError, assertQueryFieldsReadable, createDdlBootstrapper, createEmailService, logger, rawQueryLoggingEnabled, resolveBatchRefs, rowETag, splitFieldOps } from "@rebasepro/server";
23
22
  import { and, asc, count, desc, eq, getTableColumns, getTableName, gt, ilike, inArray, is, isNotNull, isNull, isTable, lt, lte, not, notInArray, or, relations, sql } from "drizzle-orm";
24
- import { PgArray, PgNumeric, PgTable, PgTimestamp, PgTimestampString, PgTransaction, bigint, boolean, char, cidr, customType, date, doublePrecision, geometry, getTableConfig, index, inet, integer, interval, json, jsonb, line, macaddr, macaddr8, numeric, pgSchema, pgTable, point, primaryKey, real, smallint, text, time, timestamp, unique, uuid, varchar, vector } from "drizzle-orm/pg-core";
23
+ import { PgArray, PgNumeric, PgTable, PgTimestamp, PgTimestampString, PgTransaction, bigint, boolean, getTableConfig, index, integer, jsonb, pgSchema, pgTable, text, timestamp, unique, uuid } from "drizzle-orm/pg-core";
25
24
  import { ALL_WHERE_FILTER_OPS, ANONYMOUS_USER_ID, DEFAULT_COMMIT_PATHS, DEFAULT_DATA_SOURCE_KEY, INCLUDE_COUNT_KEY, JUNCTION_PIVOT_KEY, ListLimitError, MAX_INCLUDE_DEPTH, Vector, encodeRelationAggregateSort, hasForeignKeyOnTarget, isChannelBusInstance, isChannelBusInstance as isChannelBusInstance$1, isManyToMany, isRelationalCollectionConfig, isSQLAdmin, parseEnvBoolean, parseRelationAggregateSort, resolveClientListLimit, resourceEnvSuffix, resourceKeyOf } from "@rebasepro/types";
26
- import { COMPOSITE_ID_SEPARATOR, COMPOSITE_ID_SEPARATOR as COMPOSITE_ID_SEPARATOR$1, CollectionRegistry, DEFAULT_ONE_OF_TYPE, DEFAULT_ONE_OF_VALUE, OrderBySpecError, REBASE_USER_ROLE, UnknownFilterOperatorError, apiHiddenFieldNames, applyDefaultValuesOnCreate, buildCompositeId, buildCompositeId as buildCompositeId$1, buildPropertyCallbacks, buildSdkData, callbackRefusal, canReadField, classifyTable, createAccessorSlugResolver, createRelationRef, createRelationRefWithData, deserializeFilter, detectJunctionTables, encodeCursor, fieldKeyForColumn, findRelation, firstSqlRow, getColumnName, getDeclaredPrimaryKeys, getJunctionCollectionConfig, getJunctionConfigForRelation, getTableName as getTableName$1, getTenantConfig, hasFieldAccessRules, includableRelationNames, isAddressableId, normalizeDriverOrderBy, normalizeEmail, normalizeInclude, normalizeToEntityRelation, parseIdValues, parseIdValues as parseIdValues$1, parseOrderBySpecStrict, queryableFieldNames, requireCallbackClient, requireCallbackCollection, resolveCollectionRelations, resolveJunctionSpecs, resolveTenantWrite, restoresSoftDeletedRow, revokeInternalTableSql, softDeleteFieldOf, sqlRows, tenantBypassRoles, toCallbackError, toFilterTuples, unknownAggregateField, unknownFilterField, unknownIncludeRelation, unknownOrderByField, unknownProjectionField, unknownRelationTargetField, unknownVectorProperty, updateDateAutoValues, updateUserAutoValues, withRelationReferencesOnly } from "@rebasepro/common";
27
- import { camelCase, firstFreeKey, generateForeignKeyName, isPrototypePollutingKey, legacyForeignKeyName, splitSearchTerms, toSnakeCase, toWireKey, unref } from "@rebasepro/utils";
25
+ import { COMPOSITE_ID_SEPARATOR, COMPOSITE_ID_SEPARATOR as COMPOSITE_ID_SEPARATOR$1, CollectionRegistry, DEFAULT_ONE_OF_TYPE, DEFAULT_ONE_OF_VALUE, OrderBySpecError, REBASE_USER_ROLE, UnknownFilterOperatorError, apiHiddenFieldNames, applyDefaultValuesOnCreate, buildCompositeId, buildCompositeId as buildCompositeId$1, buildPropertyCallbacks, buildSdkData, callbackRefusal, canReadField, classifyTable, createAccessorSlugResolver, createRelationRef, createRelationRefWithData, deserializeFilter, detectJunctionTables, encodeCursor, fieldKeyForColumn, findRelation, firstSqlRow, getColumnName, getDeclaredPrimaryKeys, getJunctionConfigForRelation, getTableName as getTableName$1, getTenantConfig, hasFieldAccessRules, includableRelationNames, isAddressableId, normalizeDriverOrderBy, normalizeEmail, normalizeInclude, normalizeToEntityRelation, parseIdValues, parseIdValues as parseIdValues$1, parseOrderBySpecStrict, queryableFieldNames, requireCallbackClient, requireCallbackCollection, resolveCollectionRelations, resolveTenantWrite, restoresSoftDeletedRow, revokeInternalTableSql, softDeleteFieldOf, sqlRows, tenantBypassRoles, toCallbackError, toFilterTuples, unknownAggregateField, unknownFilterField, unknownIncludeRelation, unknownOrderByField, unknownProjectionField, unknownRelationTargetField, unknownVectorProperty, updateDateAutoValues, updateUserAutoValues, withRelationReferencesOnly } from "@rebasepro/common";
26
+ import { camelCase, generateForeignKeyName, isPrototypePollutingKey, legacyForeignKeyName, splitSearchTerms, toSnakeCase, toWireKey, unref } from "@rebasepro/utils";
28
27
  import { AsyncLocalStorage } from "node:async_hooks";
29
28
  import { WebSocket } from "ws";
30
29
  import { EventEmitter } from "events";
@@ -8900,6 +8899,15 @@ var DEFAULT_RECONNECT_DELAY_MS = 3e3;
8900
8899
  /** Guards the identifier interpolated into `LISTEN`. */
8901
8900
  var SAFE_CHANNEL = /^[A-Za-z_][A-Za-z0-9_]*$/;
8902
8901
  /**
8902
+ * Why a connect failed, in one line: the message, and the SQLSTATE or socket
8903
+ * code when the message does not already carry it.
8904
+ */
8905
+ function connectFailureReason(err) {
8906
+ const message = err instanceof Error ? err.message : String(err);
8907
+ const code = err instanceof Error && "code" in err && typeof err.code === "string" ? err.code : void 0;
8908
+ return code && !message.includes(code) ? `${message} [${code}]` : message;
8909
+ }
8910
+ /**
8903
8911
  * A half-open connection — one whose packets stop arriving, with no FIN and no
8904
8912
  * RST, which is what an idle-flow timeout on a load balancer, a NAT, a dead
8905
8913
  * middlebox or a failed-over network looks like — fires no `error` and no
@@ -8923,6 +8931,8 @@ var PgNotifyListener = class {
8923
8931
  heartbeatInFlight = false;
8924
8932
  downSince;
8925
8933
  lastHeartbeatAt;
8934
+ /** Whether this outage's "could not be opened" has been said. Cleared once it listens again. */
8935
+ outageWarned = false;
8926
8936
  constructor(options) {
8927
8937
  this.options = options;
8928
8938
  if (!SAFE_CHANNEL.test(options.channel)) throw new Error(`Unsafe NOTIFY channel name "${options.channel}" — expected a plain SQL identifier.`);
@@ -9014,6 +9024,7 @@ var PgNotifyListener = class {
9014
9024
  this.client = client;
9015
9025
  pending = void 0;
9016
9026
  this.downSince = void 0;
9027
+ this.outageWarned = false;
9017
9028
  this.lastHeartbeatAt = Date.now();
9018
9029
  this.startHeartbeat(client);
9019
9030
  logger.debug(`📡 ${logLabel} Listening on channel "${channel}".`);
@@ -9022,7 +9033,12 @@ var PgNotifyListener = class {
9022
9033
  await pending.end();
9023
9034
  } catch {}
9024
9035
  if (first && !this.options.retryInitialConnect) throw err;
9025
- logger.error(`❌ ${logLabel} Failed to connect LISTEN client`, { error: err });
9036
+ const retry = `${logLabel} LISTEN connection could not be opened (${connectFailureReason(err)}); retrying every ${Math.round((this.options.reconnectDelayMs ?? DEFAULT_RECONNECT_DELAY_MS) / 1e3)} s.`;
9037
+ if (this.outageWarned) logger.debug(retry);
9038
+ else {
9039
+ this.outageWarned = true;
9040
+ logger.warn(`⚠️ ${retry}`);
9041
+ }
9026
9042
  this.scheduleReconnect();
9027
9043
  return;
9028
9044
  }
@@ -16485,9 +16501,10 @@ function staleCodegenDefect(table, { legacy, current }) {
16485
16501
  * table, a target belonging to another backend — because a false alarm here
16486
16502
  * costs more than a missed one: it would block boot on a working app.
16487
16503
  */
16488
- function findRelationDefects(collections, registry) {
16504
+ function findRelationDefects(collections, registry, given) {
16489
16505
  const defects = [];
16490
16506
  const registeredSlugs = new Set(registry.getCollections().map((c) => c.slug));
16507
+ const givenSlugs = given ? new Set(given.map((c) => c.slug)) : void 0;
16491
16508
  for (const collection of collections) {
16492
16509
  const sourceTableName = getTableName$1(collection);
16493
16510
  const sourceTable = registry.getTable(sourceTableName);
@@ -16515,7 +16532,11 @@ function findRelationDefects(collections, registry) {
16515
16532
  const targetTableName = getTableName$1(targetCollection);
16516
16533
  const targetTable = registry.getTable(targetTableName);
16517
16534
  if (!targetTable) {
16518
- defects.push({
16535
+ defects.push(givenSlugs && !givenSlugs.has(targetCollection.slug) ? {
16536
+ ...at,
16537
+ problem: `it points at collection \`${targetCollection.slug}\`, which is not among the collections this backend was given — it is registered only as this relation's target, so no table was built for it`,
16538
+ fix: `add the \`${targetCollection.slug}\` collection to the \`collections\` you pass, or pass every collection the relations reach`
16539
+ } : {
16519
16540
  ...at,
16520
16541
  problem: `it points at collection \`${targetCollection.slug}\`, which has no table \`${targetTableName}\` in the schema`,
16521
16542
  fix: `create the \`${targetTableName}\` table, or correct \`table\` on the \`${targetCollection.slug}\` collection`
@@ -16658,8 +16679,8 @@ function findRelationDefects(collections, registry) {
16658
16679
  * "nothing" is the kind of bug found in production, weeks later, by a user
16659
16680
  * asking where their data went.
16660
16681
  */
16661
- function assertRelationsResolve(collections, registry) {
16662
- const defects = findRelationDefects(collections, registry);
16682
+ function assertRelationsResolve(collections, registry, given) {
16683
+ const defects = findRelationDefects(collections, registry, given);
16663
16684
  if (defects.length === 0) return;
16664
16685
  const lines = defects.map((d) => ` • ${d.collection}.${d.relationName} (${d.kind})\n ${d.problem}\n fix: ${d.fix}`);
16665
16686
  throw new Error(`${defects.length} relation${defects.length === 1 ? "" : "s"} cannot resolve against the database this server is connected to.
@@ -16697,7 +16718,7 @@ function buildCollectionRegistry(schema) {
16697
16718
  if (schema.enums) registry.registerEnums(schema.enums);
16698
16719
  if (schema.relations) registry.registerRelations(schema.relations);
16699
16720
  warnOnKeysTheAdminCannotResolve(registry.getCollections(), registry);
16700
- assertRelationsResolve(registry.getCollections(), registry);
16721
+ assertRelationsResolve(registry.getCollections(), registry, schema.collections);
16701
16722
  return registry;
16702
16723
  }
16703
16724
  //#endregion
@@ -18753,991 +18774,6 @@ function patchPgNumericToNumber(tables) {
18753
18774
  if (patchedCount > 0) logger.debug(`[PgNumeric] ${patchedCount} numeric column(s) will be served as numbers`);
18754
18775
  }
18755
18776
  //#endregion
18756
- //#region src/schema/introspect-runtime.ts
18757
- /**
18758
- * Read the RLS posture of each table in a schema.
18759
- *
18760
- * This is what decides whether baas mode may serve a table. A table with RLS
18761
- * disabled has no authorization model: since every authenticated request runs
18762
- * as `rebase_user`, and that role is granted DML on the schema, serving such a
18763
- * table hands every row to every logged-in user.
18764
- */
18765
- async function readRlsStatus(client, pgSchema) {
18766
- const { rows } = await client.query(`SELECT c.relname AS table,
18767
- c.relrowsecurity AS rls_enabled,
18768
- (SELECT count(*) FROM pg_policy p WHERE p.polrelid = c.oid) AS policy_count
18769
- FROM pg_class c
18770
- JOIN pg_namespace n ON n.oid = c.relnamespace
18771
- WHERE n.nspname = $1 AND c.relkind = 'r'`, [pgSchema]);
18772
- return new Map(rows.map((r) => [r.table, {
18773
- table: r.table,
18774
- rlsEnabled: r.rls_enabled === true,
18775
- policyCount: Number(r.policy_count ?? 0)
18776
- }]));
18777
- }
18778
- /**
18779
- * Read tables, columns, enums, primary keys and foreign keys for a schema.
18780
- * Mirrors the queries in introspect-db.ts.
18781
- */
18782
- async function introspectSchema(client, pgSchema) {
18783
- const { rows: tables } = await client.query(`SELECT table_name
18784
- FROM information_schema.tables
18785
- WHERE table_schema = $1 AND table_type = 'BASE TABLE'
18786
- AND table_name NOT LIKE 'drizzle_%'
18787
- AND table_name NOT LIKE 'rebase_%'
18788
- ORDER BY table_name`, [pgSchema]);
18789
- const { rows: columns } = await client.query(`SELECT
18790
- c.table_name,
18791
- c.column_name,
18792
- c.data_type,
18793
- c.udt_name,
18794
- c.is_nullable,
18795
- c.column_default,
18796
- -- A stored generated column must be declared as one, or drizzle
18797
- -- writes DEFAULT into it on every INSERT. Postgres tolerates that
18798
- -- today; the generated module has always declared them properly,
18799
- -- and the runtime tables are built to match it.
18800
- c.is_generated,
18801
- c.generation_expression,
18802
- (SELECT a.atttypmod FROM pg_attribute a
18803
- JOIN pg_class pc ON a.attrelid = pc.oid
18804
- WHERE pc.relname = c.table_name
18805
- AND a.attname = c.column_name
18806
- AND pc.relnamespace = (SELECT oid FROM pg_namespace WHERE nspname = c.table_schema)) as atttypmod
18807
- FROM information_schema.columns c
18808
- WHERE c.table_schema = $1`, [pgSchema]);
18809
- const { rows: enumValues } = await client.query(`SELECT t.typname AS enum_name,
18810
- e.enumlabel AS enum_value,
18811
- e.enumsortorder AS sort_order
18812
- FROM pg_type t
18813
- JOIN pg_enum e ON t.oid = e.enumtypid
18814
- JOIN pg_namespace n ON t.typnamespace = n.oid
18815
- WHERE n.nspname = $1
18816
- ORDER BY t.typname, e.enumsortorder`, [pgSchema]);
18817
- const { rows: pks } = await client.query(`SELECT t.relname as table_name, a.attname as column_name
18818
- FROM pg_index i
18819
- JOIN pg_attribute a ON a.attrelid = i.indrelid
18820
- AND a.attnum = ANY(i.indkey)
18821
- JOIN pg_class t ON t.oid = i.indrelid
18822
- JOIN pg_namespace n ON n.oid = t.relnamespace
18823
- WHERE i.indisprimary AND n.nspname = $1`, [pgSchema]);
18824
- const { rows: fks } = await client.query(`SELECT
18825
- tc.table_name,
18826
- kcu.column_name,
18827
- ccu.table_name AS foreign_table_name,
18828
- ccu.column_name AS foreign_column_name
18829
- FROM information_schema.table_constraints AS tc
18830
- JOIN information_schema.key_column_usage AS kcu
18831
- ON tc.constraint_name = kcu.constraint_name
18832
- AND tc.table_schema = kcu.table_schema
18833
- JOIN information_schema.constraint_column_usage AS ccu
18834
- ON ccu.constraint_name = tc.constraint_name
18835
- AND ccu.table_schema = tc.table_schema
18836
- WHERE tc.constraint_type = 'FOREIGN KEY' AND tc.table_schema = $1`, [pgSchema]);
18837
- const tablesMap = buildTablesMap(tables, columns, pks, fks);
18838
- return {
18839
- tablesMap,
18840
- enumMap: buildEnumMap(enumValues),
18841
- joinTables: identifyJoinTables(tablesMap)
18842
- };
18843
- }
18844
- /** Derive the `isId` flavour for a primary-key column. */
18845
- function idKindFor(col, propType) {
18846
- if (col.data_type.toLowerCase() === "uuid") return "uuid";
18847
- if (propType === "number") return "increment";
18848
- return true;
18849
- }
18850
- function buildProperties(meta, enumMap) {
18851
- const properties = {};
18852
- const takenKeys = /* @__PURE__ */ new Set();
18853
- for (const col of meta.columns) {
18854
- const isPk = meta.pks.includes(col.column_name);
18855
- if (meta.fks.some((fk) => fk.column_name === col.column_name) && !isPk) continue;
18856
- const enumValues = enumMap.get(col.udt_name);
18857
- const isEnum = col.data_type === "USER-DEFINED" && enumValues !== void 0;
18858
- const isVector = col.udt_name === "vector";
18859
- const propType = isEnum ? "string" : isVector ? "vector" : mapPgType(col.data_type);
18860
- const property = {
18861
- name: humanize(col.column_name),
18862
- columnName: col.column_name,
18863
- type: propType
18864
- };
18865
- const key = firstFreeKey([toWireKey(col.column_name), col.column_name], takenKeys);
18866
- takenKeys.add(key);
18867
- if (isPk) property.isId = idKindFor(col, propType);
18868
- else if (col.is_nullable === "NO" && col.column_default === null) property.validation = { required: true };
18869
- if (isEnum && enumValues) property.enum = enumValues.map((value) => ({
18870
- id: value,
18871
- label: humanize(value)
18872
- }));
18873
- properties[key] = property;
18874
- }
18875
- return properties;
18876
- }
18877
- /**
18878
- * Owning relations, derived from this table's foreign keys — the same shape
18879
- * `generateCollectionFile` writes into a collection file: a `relation` property
18880
- * whose nested descriptor carries `kind`, a `target` thunk and the `localKey`.
18881
- *
18882
- * The shape is load-bearing, not cosmetic. `resolveCollectionRelations` reads
18883
- * relations from `property.relation` and `resolveRelation` requires `target` to
18884
- * be a thunk; this used to emit `target`/`cardinality`/`localKey` flat on the
18885
- * property with the slug as a bare string, which satisfies neither. Nothing
18886
- * threw — the resolver simply skipped every such property and reported that the
18887
- * collection had no relations. So an introspected BaaS collection had its FK
18888
- * columns removed from `properties` (they "surface as relations") and then no
18889
- * resolvable relation to surface as, which is why writing the FK column
18890
- * directly came back as `has no field 'product_id'`: `assertKnownWriteFields`
18891
- * learns that column from the resolved relation's `localKey`.
18892
- *
18893
- * The thunk closes over the collections being built in this same pass rather
18894
- * than importing a module, which is what a runtime introspection has instead of
18895
- * generated files. It is called lazily, after the map is fully populated, so a
18896
- * table may reference one introspected later.
18897
- */
18898
- function buildRelations(meta, slugByTable, collectionBySlug) {
18899
- const relations = {};
18900
- for (const fk of meta.fks) {
18901
- const targetSlug = slugByTable.get(fk.foreign_table_name);
18902
- if (!targetSlug) continue;
18903
- let key = toWireKey(fk.column_name.replace(/_id$/, ""));
18904
- if (meta.pks.includes(fk.column_name) && key === fk.column_name) key = fk.foreign_table_name;
18905
- relations[key] = {
18906
- name: humanize(key),
18907
- type: "relation",
18908
- relation: {
18909
- kind: "belongsTo",
18910
- target: () => collectionBySlug.get(targetSlug),
18911
- localKey: fk.column_name
18912
- }
18913
- };
18914
- }
18915
- return relations;
18916
- }
18917
- /**
18918
- * Turn an introspected schema into collections.
18919
- *
18920
- * Join tables are skipped: they carry no identity of their own and exist to
18921
- * express a many-to-many edge between two other tables.
18922
- */
18923
- function buildCollectionsFromSchema({ tablesMap, enumMap, joinTables }, pgSchema) {
18924
- const slugByTable = /* @__PURE__ */ new Map();
18925
- for (const tableName of tablesMap.keys()) if (!joinTables.has(tableName)) slugByTable.set(tableName, tableName);
18926
- const collections = [];
18927
- const collectionBySlug = /* @__PURE__ */ new Map();
18928
- for (const [tableName, meta] of tablesMap) {
18929
- if (joinTables.has(tableName)) continue;
18930
- const collectionName = humanize(tableName);
18931
- const collection = {
18932
- name: collectionName,
18933
- singularName: singularize(collectionName),
18934
- slug: tableName,
18935
- table: tableName,
18936
- schema: pgSchema,
18937
- icon: getIconForTable(tableName),
18938
- properties: {
18939
- ...buildProperties(meta, enumMap),
18940
- ...buildRelations(meta, slugByTable, collectionBySlug)
18941
- }
18942
- };
18943
- collections.push(collection);
18944
- collectionBySlug.set(tableName, collection);
18945
- }
18946
- return collections;
18947
- }
18948
- //#endregion
18949
- //#region src/schema/dynamic-tables.ts
18950
- /**
18951
- * Build drizzle tables at runtime from an introspected schema.
18952
- *
18953
- * **Every** data source's tables are built here, from `information_schema`,
18954
- * after boot-ensure has run — a project with declared collections included. The
18955
- * committed `schema.generated.ts` is a build artefact for Atlas, `db push`,
18956
- * `eject` and user code; the runtime does not read it for its tables, because a
18957
- * file that is one commit behind the database used to change what the server
18958
- * served (a renamed foreign-key column 500'd every request, a lost relation
18959
- * silently emptied a list). See `catalogue-schema.ts` for the caller.
18960
- *
18961
- * These are handed to drizzle as its schema, which keeps the relational query
18962
- * path (`db.query.*`) working; without them FetchService would fall back to
18963
- * plain selects and lose relation loading.
18964
- *
18965
- * Two things a caller has to supply for the tables to match what the generated
18966
- * module used to produce, both about *names*: the Drizzle object is keyed by the
18967
- * wire name of a column and not by the column (`authorId`, not `author_id`), and
18968
- * a relation is keyed by the collection's own relation key rather than by a
18969
- * guess made from the foreign key. Both come from the collections — see
18970
- * `columnKeysFromCollections` and `buildDrizzleRelationsFromCollections`.
18971
- */
18972
- var columnNameAsKey = (_table, column) => column;
18973
- /** drizzle ships no bytea builder; binary must round-trip as a Buffer. */
18974
- var bytea = customType({ dataType: () => "bytea" });
18975
- /**
18976
- * A full-text search column, declared with its real SQL type.
18977
- *
18978
- * Not cosmetic: `isSearchIndexColumn` asks the column for its type and drops
18979
- * every `tsvector` from the projection, so a search column that fell through to
18980
- * `text` (the old default for an unrecognised type) would be selected, returned
18981
- * to callers, and rendered in the admin as a wall of lexemes. drizzle ships no
18982
- * builder for these two.
18983
- */
18984
- var tsvector = customType({ dataType: () => "tsvector" });
18985
- var tsquery = customType({ dataType: () => "tsquery" });
18986
- /**
18987
- * Postgres stores a column's type modifier (varchar length, vector dimensions)
18988
- * in `atttypmod`. For length-carrying string types it is length + VARHDRSZ(4);
18989
- * for pgvector it is the dimension count as-is. -1 means unspecified.
18990
- */
18991
- function varlenLength(col) {
18992
- return col.atttypmod && col.atttypmod > 4 ? col.atttypmod - 4 : void 0;
18993
- }
18994
- /**
18995
- * Map a Postgres type to a drizzle column builder, keyed on `udt_name` — the
18996
- * concrete underlying type, which is exact where `data_type` reports umbrella
18997
- * values like "ARRAY" or "USER-DEFINED".
18998
- *
18999
- * Unknown types fall back to `text`: the driver still reads and writes them,
19000
- * with the value passing through as a string, which beats dropping the column.
19001
- */
19002
- function scalarBuilder(udtName, name, col) {
19003
- switch (udtName) {
19004
- case "uuid": return uuid(name);
19005
- case "bool": return boolean(name);
19006
- case "int2": return smallint(name);
19007
- case "int4": return integer(name);
19008
- case "int8": return bigint(name, { mode: "number" });
19009
- case "float4": return real(name);
19010
- case "float8": return doublePrecision(name);
19011
- case "numeric":
19012
- case "money": return numeric(name);
19013
- case "json": return json(name);
19014
- case "jsonb": return jsonb(name);
19015
- case "date": return date(name);
19016
- case "time": return time(name);
19017
- case "timetz": return time(name, { withTimezone: true });
19018
- case "timestamp": return timestamp(name, { mode: "string" });
19019
- case "timestamptz": return timestamp(name, {
19020
- withTimezone: true,
19021
- mode: "string"
19022
- });
19023
- case "interval": return interval(name);
19024
- case "bytea": return bytea(name);
19025
- case "inet": return inet(name);
19026
- case "cidr": return cidr(name);
19027
- case "macaddr": return macaddr(name);
19028
- case "macaddr8": return macaddr8(name);
19029
- case "point": return point(name);
19030
- case "line": return line(name);
19031
- case "geometry": return geometry(name);
19032
- case "vector": {
19033
- const dimensions = col.atttypmod && col.atttypmod > 0 ? col.atttypmod : void 0;
19034
- return dimensions ? vector(name, { dimensions }) : text(name);
19035
- }
19036
- case "bpchar": {
19037
- const length = varlenLength(col);
19038
- return length ? char(name, { length }) : char(name);
19039
- }
19040
- case "varchar": {
19041
- const length = varlenLength(col);
19042
- return length ? varchar(name, { length }) : text(name);
19043
- }
19044
- case "tsvector": return tsvector(name);
19045
- case "tsquery": return tsquery(name);
19046
- default: return text(name);
19047
- }
19048
- }
19049
- /** See {@link FluentColumnBuilder}. */
19050
- var fluent = (builder) => builder;
19051
- function columnBuilderFor(col) {
19052
- if (col.udt_name.startsWith("_")) return fluent(scalarBuilder(col.udt_name.slice(1), col.column_name, col)).array();
19053
- return scalarBuilder(col.udt_name, col.column_name, col);
19054
- }
19055
- /**
19056
- * Build one drizzle table per introspected table, keyed by table name.
19057
- *
19058
- * @param columnKey how a column is named on the wire — see {@link ColumnKeyResolver}.
19059
- */
19060
- function buildDrizzleTablesFromSchema(tablesMap, pgSchemaName = "public", columnKey = columnNameAsKey) {
19061
- const schema = pgSchemaName === "public" ? null : pgSchema(pgSchemaName);
19062
- const createTable = schema ? schema.table.bind(schema) : pgTable;
19063
- const tables = {};
19064
- for (const [tableName, meta] of tablesMap) {
19065
- const columns = {};
19066
- const keyOf = /* @__PURE__ */ new Map();
19067
- for (const col of meta.columns) {
19068
- let builder = columnBuilderFor(col);
19069
- if (col.is_nullable === "NO") builder = fluent(builder).notNull();
19070
- if (meta.pks.length === 1 && meta.pks[0] === col.column_name) builder = fluent(builder).primaryKey();
19071
- if (col.is_generated === "ALWAYS") builder = fluent(builder).generatedAlwaysAs(sql.raw(col.generation_expression ?? ""));
19072
- const key = columnKey(tableName, col.column_name);
19073
- keyOf.set(col.column_name, key);
19074
- columns[key] = builder;
19075
- }
19076
- tables[tableName] = createTable(tableName, columns, meta.pks.length > 1 ? (t) => [primaryKey({ columns: meta.pks.map((pk) => t[keyOf.get(pk) ?? pk]) })] : void 0);
19077
- }
19078
- return tables;
19079
- }
19080
- /**
19081
- * Build drizzle `relations()` for the foreign keys, so the relational query
19082
- * path can actually load them.
19083
- *
19084
- * FetchService asks drizzle for `with: { <key>: true }`, keyed by the relation
19085
- * property on the collection. Tables alone don't satisfy that — without these,
19086
- * `?include=author` silently returns the raw `author_id` and no author.
19087
- *
19088
- * The keys here must match `buildRelations` in introspect-runtime.ts, which is
19089
- * what names the collection's relation properties.
19090
- */
19091
- function buildDrizzleRelationsFromSchema(tablesMap, tables, columnKey = columnNameAsKey) {
19092
- /** Owning side, per table: the `one()` relations from its foreign keys. */
19093
- const owning = /* @__PURE__ */ new Map();
19094
- /** Inverse side, per referenced table: the matching `many()` back-references. */
19095
- const inverse = /* @__PURE__ */ new Map();
19096
- for (const [tableName, meta] of tablesMap) {
19097
- if (!tables[tableName]) continue;
19098
- const columnNames = new Set(meta.columns.map((c) => columnKey(tableName, c.column_name)));
19099
- for (const fk of meta.fks) {
19100
- if (!tables[fk.foreign_table_name]) continue;
19101
- let key = toWireKey(fk.column_name.replace(/_id$/, ""));
19102
- if (meta.pks.includes(fk.column_name) && key === fk.column_name) key = fk.foreign_table_name;
19103
- if (columnNames.has(key)) continue;
19104
- const relationName = `${tableName}_${fk.column_name}`;
19105
- owning.set(tableName, [...owning.get(tableName) ?? [], {
19106
- key,
19107
- targetTable: fk.foreign_table_name,
19108
- fkColumn: columnKey(tableName, fk.column_name),
19109
- targetColumn: columnKey(fk.foreign_table_name, fk.foreign_column_name),
19110
- relationName
19111
- }]);
19112
- const backKey = tableName;
19113
- if (new Set((tablesMap.get(fk.foreign_table_name)?.columns ?? []).map((c) => columnKey(fk.foreign_table_name, c.column_name))).has(backKey)) continue;
19114
- inverse.set(fk.foreign_table_name, [...inverse.get(fk.foreign_table_name) ?? [], {
19115
- key: backKey,
19116
- sourceTable: tableName,
19117
- relationName
19118
- }]);
19119
- }
19120
- }
19121
- const built = {};
19122
- for (const tableName of tablesMap.keys()) {
19123
- const table = tables[tableName];
19124
- const ones = owning.get(tableName) ?? [];
19125
- const manys = inverse.get(tableName) ?? [];
19126
- if (!table || ones.length === 0 && manys.length === 0) continue;
19127
- built[`${tableName}Relations`] = relations(table, ({ one, many }) => {
19128
- const map = {};
19129
- for (const rel of ones) map[rel.key] = one(tables[rel.targetTable], {
19130
- fields: [getTableColumns(table)[rel.fkColumn]],
19131
- references: [getTableColumns(tables[rel.targetTable])[rel.targetColumn]],
19132
- relationName: rel.relationName
19133
- });
19134
- for (const rel of manys) {
19135
- if (map[rel.key]) continue;
19136
- map[rel.key] = many(tables[rel.sourceTable], { relationName: rel.relationName });
19137
- }
19138
- return map;
19139
- });
19140
- }
19141
- return built;
19142
- }
19143
- //#endregion
19144
- //#region src/schema/config-relations.ts
19145
- /**
19146
- * Build drizzle `relations()` for declared collections, from the collections.
19147
- *
19148
- * The heuristic builder in `dynamic-tables.ts` names a relation after the
19149
- * foreign-key column (`author_id` → `author`), which is the only evidence a
19150
- * database offers and is right for BaaS mode. It is wrong for a project that
19151
- * declares its collections: there the relation's name is whatever the author
19152
- * called it, `include=writer` addresses that name, and a relation whose key was
19153
- * guessed from the column simply does not answer to it — the read returns the
19154
- * bare foreign key and no target, silently, which is how a demo lost every
19155
- * relation in its list view while the detail view (a different code path) kept
19156
- * working.
19157
- *
19158
- * So when there are collections, they are the authority for *which* relations
19159
- * exist and what they are called; the live catalogue is the authority for the
19160
- * columns underneath. This is the same pairing `generate-drizzle-schema-logic`
19161
- * emits into `schema.generated.ts` — kind for kind, key for key, and paired by
19162
- * the same {@link sharedRelationName} — only built against tables read back from
19163
- * `information_schema` rather than against a file.
19164
- */
19165
- /** A collection's table, with any schema prefix stripped — as the DDL creates it. */
19166
- function bareTableName(name) {
19167
- return name.includes(".") ? name.split(".").pop() : name;
19168
- }
19169
- /**
19170
- * The field key a collection's rows are addressed by, as the generated module
19171
- * spells it: the property marked `isId`, else `id`.
19172
- *
19173
- * Deliberately the config's answer rather than the catalogue's primary key —
19174
- * `references: [users.id]` in the generated file is keyed the same way, and the
19175
- * two have to name the same Drizzle key for a relation to compile at all.
19176
- */
19177
- function primaryKeyFieldKey(collection) {
19178
- for (const [key, prop] of Object.entries(collection.properties ?? {})) if (prop && typeof prop === "object" && "isId" in prop && Boolean(prop.isId)) return key;
19179
- return "id";
19180
- }
19181
- /**
19182
- * The field key on the source table that a `hasOne`/`hasMany` foreign key
19183
- * holds: its `sourceKey` when the link names one, else the primary key.
19184
- */
19185
- function sourceKeyFieldKey(relation, collection) {
19186
- return relation.sourceKey ? fieldKeyForColumn(collection, relation.sourceKey) : primaryKeyFieldKey(collection);
19187
- }
19188
- /**
19189
- * A column of a built table, by its Drizzle key or by its column name.
19190
- *
19191
- * The key is the answer in the ordinary case, and the name is the fallback for
19192
- * the one place a caller holds a column and not a field: a junction table's two
19193
- * foreign keys, which are keyed by column *unless* the project also declares a
19194
- * collection over that table, in which case they are keyed by its property
19195
- * names. Looking both up costs a scan of one table and removes the difference.
19196
- */
19197
- function columnOf(table, key) {
19198
- if (!table) return void 0;
19199
- const direct = getTableColumns(table)[key];
19200
- if (direct) return direct;
19201
- for (const column of Object.values(getTableColumns(table))) if (column?.name === key) return column;
19202
- }
19203
- /**
19204
- * Drizzle relations for every declared collection, keyed `<table>Relations`.
19205
- *
19206
- * @param collections every collection backed by a table in `tables`
19207
- * @param tables catalogue-built tables, keyed by bare table name
19208
- */
19209
- function buildDrizzleRelationsFromCollections(collections, tables) {
19210
- const ones = /* @__PURE__ */ new Map();
19211
- const manys = /* @__PURE__ */ new Map();
19212
- /** `<relationName>::<kind>` already emitted on a table — the generator dedupes the same way. */
19213
- const emitted = /* @__PURE__ */ new Map();
19214
- const claim = (table, name, kind) => {
19215
- const seen = emitted.get(table) ?? /* @__PURE__ */ new Set();
19216
- emitted.set(table, seen);
19217
- const key = `${name}::${kind}`;
19218
- if (seen.has(key)) return false;
19219
- seen.add(key);
19220
- return true;
19221
- };
19222
- const addOne = (table, spec) => ones.set(table, [...ones.get(table) ?? [], spec]);
19223
- const addMany = (table, spec) => manys.set(table, [...manys.get(table) ?? [], spec]);
19224
- const byTable = /* @__PURE__ */ new Map();
19225
- for (const collection of collections) {
19226
- const table = bareTableName(getTableName$1(collection));
19227
- if (table && tables[table]) byTable.set(table, collection);
19228
- }
19229
- for (const [tableName, collection] of byTable) {
19230
- const table = tables[tableName];
19231
- for (const [relationKey, relation] of Object.entries(resolveCollectionRelations(collection))) {
19232
- let target;
19233
- try {
19234
- target = relation.target();
19235
- } catch (err) {
19236
- logger.debug(`[schema] Relation ${collection.slug}.${relationKey} has no resolvable target`, { error: err });
19237
- continue;
19238
- }
19239
- const targetTable = tables[bareTableName(getTableName$1(target))];
19240
- const name = sharedRelationName(relation, collection);
19241
- switch (relation.kind) {
19242
- case "belongsTo": {
19243
- if (!targetTable) continue;
19244
- if (!claim(tableName, name, relation.kind)) continue;
19245
- const localKey = fieldKeyForColumn(collection, relation.localKey);
19246
- const referenced = primaryKeyFieldKey(target);
19247
- const field = columnOf(table, localKey);
19248
- const reference = columnOf(targetTable, referenced);
19249
- if (!field || !reference) continue;
19250
- addOne(tableName, {
19251
- key: relationKey,
19252
- target: targetTable,
19253
- fields: [field],
19254
- references: [reference],
19255
- relationName: name
19256
- });
19257
- break;
19258
- }
19259
- case "hasOne": {
19260
- if (!targetTable) continue;
19261
- if (!claim(tableName, name, relation.kind)) continue;
19262
- const field = columnOf(table, sourceKeyFieldKey(relation, collection));
19263
- const reference = columnOf(targetTable, fieldKeyForColumn(target, relation.foreignKeyOnTarget));
19264
- if (!field || !reference) continue;
19265
- addOne(tableName, {
19266
- key: relationKey,
19267
- target: targetTable,
19268
- fields: [field],
19269
- references: [reference],
19270
- relationName: name
19271
- });
19272
- break;
19273
- }
19274
- case "hasMany":
19275
- if (!targetTable) continue;
19276
- if (!claim(tableName, name, relation.kind)) continue;
19277
- addMany(tableName, {
19278
- key: relationKey,
19279
- target: targetTable,
19280
- relationName: name
19281
- });
19282
- break;
19283
- case "manyToMany": {
19284
- const junctionName = bareTableName(relation.through.table);
19285
- const junction = tables[junctionName];
19286
- if (!junction) continue;
19287
- if (!claim(tableName, name, relation.kind)) continue;
19288
- addMany(tableName, {
19289
- key: relationKey,
19290
- target: junction,
19291
- relationName: name
19292
- });
19293
- addJunctionSides(relation, collection, target, junctionName, tables, addOne, claim);
19294
- break;
19295
- }
19296
- case "via": break;
19297
- }
19298
- }
19299
- }
19300
- for (const [tableName, collection] of byTable) {
19301
- const table = tables[tableName];
19302
- for (const [, relation] of Object.entries(resolveCollectionRelations(collection))) {
19303
- if (relation.kind !== "hasMany" && relation.kind !== "hasOne") continue;
19304
- let target;
19305
- try {
19306
- target = relation.target();
19307
- } catch {
19308
- continue;
19309
- }
19310
- const targetTableName = bareTableName(getTableName$1(target));
19311
- const targetTable = tables[targetTableName];
19312
- if (!targetTable) continue;
19313
- const name = sharedRelationName(relation, collection);
19314
- if (!claim(targetTableName, name, "belongsTo")) continue;
19315
- const fkKey = fieldKeyForColumn(target, relation.foreignKeyOnTarget);
19316
- const field = columnOf(targetTable, fkKey);
19317
- const reference = columnOf(table, sourceKeyFieldKey(relation, collection));
19318
- if (!field || !reference) continue;
19319
- addOne(targetTableName, {
19320
- key: `_synth_${targetTableName}_${fkKey}`,
19321
- target: tables[tableName],
19322
- fields: [field],
19323
- references: [reference],
19324
- relationName: name
19325
- });
19326
- }
19327
- }
19328
- const built = {};
19329
- for (const tableName of /* @__PURE__ */ new Set([...ones.keys(), ...manys.keys()])) {
19330
- const table = tables[tableName];
19331
- if (!table) continue;
19332
- const tableOnes = ones.get(tableName) ?? [];
19333
- const tableManys = manys.get(tableName) ?? [];
19334
- built[`${tableName}Relations`] = relations(table, ({ one, many }) => {
19335
- const map = {};
19336
- for (const spec of tableOnes) map[spec.key] = one(spec.target, {
19337
- fields: spec.fields,
19338
- references: spec.references,
19339
- relationName: spec.relationName
19340
- });
19341
- for (const spec of tableManys) {
19342
- if (map[spec.key]) continue;
19343
- map[spec.key] = many(spec.target, { relationName: spec.relationName });
19344
- }
19345
- return map;
19346
- });
19347
- }
19348
- return built;
19349
- }
19350
- /**
19351
- * The junction table's own two `one()` sides.
19352
- *
19353
- * A many-to-many is a `many(junction)` on each endpoint, so the junction has to
19354
- * carry the matching `one()` back to each — without them drizzle can join a
19355
- * post to its `posts_tags` rows and no further, and the tags come back as
19356
- * junction rows. Keyed by the junction's own columns, which is how the
19357
- * generated file keys them too.
19358
- */
19359
- function addJunctionSides(relation, sourceCollection, targetCollection, junctionName, tables, addOne, claim) {
19360
- const junction = tables[junctionName];
19361
- const sourceTable = tables[bareTableName(getTableName$1(sourceCollection))];
19362
- const targetTable = tables[bareTableName(getTableName$1(targetCollection))];
19363
- if (!junction || !sourceTable || !targetTable) return;
19364
- const sourceName = sharedRelationName(relation, sourceCollection);
19365
- const inverse = Object.values(resolveCollectionRelations(targetCollection)).find((r) => r.kind === "manyToMany" && bareTableName(r.through.table) === junctionName);
19366
- const targetName = inverse ? sharedRelationName(inverse, targetCollection) : `${junctionName}_${relation.through.targetColumn}`;
19367
- const pairs = [[
19368
- relation.through.sourceColumn,
19369
- sourceTable,
19370
- sourceName,
19371
- sourceCollection,
19372
- "source"
19373
- ], [
19374
- relation.through.targetColumn,
19375
- targetTable,
19376
- targetName,
19377
- targetCollection,
19378
- "target"
19379
- ]];
19380
- for (const [column, endpointTable, relationName, endpointCollection] of pairs) {
19381
- if (!claim(junctionName, relationName, "junction")) continue;
19382
- const field = columnOf(junction, column);
19383
- const reference = columnOf(endpointTable, primaryKeyFieldKey(endpointCollection));
19384
- if (!field || !reference) continue;
19385
- addOne(junctionName, {
19386
- key: column,
19387
- target: endpointTable,
19388
- fields: [field],
19389
- references: [reference],
19390
- relationName
19391
- });
19392
- }
19393
- }
19394
- //#endregion
19395
- //#region src/schema/catalogue-schema.ts
19396
- /** The Postgres schema a collection's table lives in. */
19397
- function schemaOfCollection(collection, fallback) {
19398
- const declared = collection.schema;
19399
- return typeof declared === "string" && declared.length > 0 ? declared : fallback;
19400
- }
19401
- /**
19402
- * How a column of each table is keyed in the Drizzle object.
19403
- *
19404
- * The wire name, which is the property key — `authorId`, never `author_id` —
19405
- * because that is what every read and write path in this driver indexes tables
19406
- * and rows by, and what the generated module has always emitted. Getting this
19407
- * wrong does not throw: drizzle leaves an unknown key out of the statement and
19408
- * the write reports success having stored nothing.
19409
- *
19410
- * Two exceptions, both columns no property declares:
19411
- *
19412
- * - the generated search columns keep their raw names, because `search_vector`
19413
- * is how `searchColumnNames` excludes them from responses and how the
19414
- * condition builder finds the one to match against;
19415
- * - a table no collection claims (a junction) is keyed by its **key** columns'
19416
- * names, which is also what the generated file does for them — but a
19417
- * junction that declares `through.properties` keys those payload columns by
19418
- * their property key, for the same reason a collection does. `joinedAt` with
19419
- * `columnName: "joined_at"` has to be one name on both sides or a `_pivot`
19420
- * write is silently dropped by drizzle and the read serves a key the caller
19421
- * never declared.
19422
- */
19423
- function columnKeysFromCollections(collections) {
19424
- const byTable = /* @__PURE__ */ new Map();
19425
- const rawColumns = /* @__PURE__ */ new Map();
19426
- for (const collection of collections) {
19427
- const table = bareTableName(getTableName$1(collection));
19428
- if (!table) continue;
19429
- byTable.set(table, collection);
19430
- rawColumns.set(table, new Set(searchColumnNames(collection)));
19431
- }
19432
- const junctionByTable = /* @__PURE__ */ new Map();
19433
- for (const spec of resolveJunctionSpecs(collections).values()) {
19434
- if (Object.keys(spec.properties).length === 0) continue;
19435
- junctionByTable.set(bareTableName(spec.table), getJunctionCollectionConfig(spec));
19436
- }
19437
- return (tableName, columnName) => {
19438
- const collection = byTable.get(tableName);
19439
- if (!collection) {
19440
- const junction = junctionByTable.get(tableName);
19441
- return junction ? fieldKeyForColumn(junction, columnName) : columnName;
19442
- }
19443
- if (rawColumns.get(tableName)?.has(columnName)) return columnName;
19444
- return fieldKeyForColumn(collection, columnName);
19445
- };
19446
- }
19447
- /**
19448
- * Read the tables this data source serves out of `information_schema`.
19449
- *
19450
- * Restricted to the collections handed in, because the driver is given every
19451
- * collection the project declares while each database holds only its own: an
19452
- * unfiltered read had a second source (`database("analytics")`) reporting the
19453
- * default's tables as its own. Junction tables come too — they back no
19454
- * collection and a many-to-many is unreadable without them.
19455
- *
19456
- * Every schema the collections name is read, not just `public`. The users
19457
- * collection lives in `rebase`, and a run restricted to `public` would leave the
19458
- * registry with no table for it at all.
19459
- */
19460
- async function readCatalogueSchema(options) {
19461
- const { client, collections, defaultSchema } = options;
19462
- /** schema → the tables wanted from it. */
19463
- const wanted = /* @__PURE__ */ new Map();
19464
- const want = (schema, table) => {
19465
- const set = wanted.get(schema) ?? /* @__PURE__ */ new Set();
19466
- wanted.set(schema, set);
19467
- set.add(table);
19468
- };
19469
- for (const collection of collections) {
19470
- const table = bareTableName(getTableName$1(collection));
19471
- if (table) want(schemaOfCollection(collection, defaultSchema), table);
19472
- }
19473
- for (const spec of resolveJunctionSpecs(collections).values()) want(spec.schema, bareTableName(spec.table));
19474
- const columnKey = columnKeysFromCollections(collections);
19475
- const tables = {};
19476
- const meta = /* @__PURE__ */ new Map();
19477
- const missing = [];
19478
- for (const [schema, names] of wanted) {
19479
- const live = await introspectSchema(client, schema);
19480
- const present = /* @__PURE__ */ new Map();
19481
- for (const name of names) {
19482
- const found = live.tablesMap.get(name);
19483
- if (!found) {
19484
- missing.push(schema === defaultSchema ? name : `${schema}.${name}`);
19485
- continue;
19486
- }
19487
- if (tables[name]) continue;
19488
- present.set(name, found);
19489
- meta.set(name, found);
19490
- }
19491
- Object.assign(tables, buildDrizzleTablesFromSchema(present, schema, columnKey));
19492
- }
19493
- return {
19494
- tables,
19495
- relations: buildDrizzleRelationsFromCollections(collections, tables),
19496
- meta,
19497
- missing
19498
- };
19499
- }
19500
- /**
19501
- * The BaaS variant: no collections are declared, so every table in one schema is
19502
- * served and both the collections and the relations are derived from the
19503
- * foreign keys.
19504
- *
19505
- * Kept apart from {@link readCatalogueSchema} because the two answer different
19506
- * questions. Here the database is the entire specification; there it supplies
19507
- * the columns for a specification the project already wrote down.
19508
- */
19509
- function buildBaasSchema(tablesMap, pgSchemaName, collections) {
19510
- const columnKey = columnKeysFromCollections(collections);
19511
- const tables = buildDrizzleTablesFromSchema(tablesMap, pgSchemaName, columnKey);
19512
- return {
19513
- tables,
19514
- relations: buildDrizzleRelationsFromSchema(tablesMap, tables, columnKey)
19515
- };
19516
- }
19517
- //#endregion
19518
- //#region src/schema/generated-schema-diff.ts
19519
- /**
19520
- * Compare the committed `schema.generated.ts` against the live catalogue, and
19521
- * say so — once per difference, at boot, as a warning and nothing more.
19522
- *
19523
- * The runtime stopped reading that file for its tables (see
19524
- * `catalogue-schema.ts`), which removes an entire class of production failure
19525
- * and creates a smaller one: a stale file is now *invisible*. It is still real —
19526
- * it is what Atlas plans migrations from, what `db push` diffs, what `eject`
19527
- * writes out and what user code imports — so a project whose file no longer
19528
- * describes its database will hit it later, in a tool, with no clue that boot
19529
- * already knew.
19530
- *
19531
- * Hence this: a guard rather than a doc. It never fails boot and never changes
19532
- * what is served — the previous arrangement, where the file could do both,
19533
- * is exactly what was removed.
19534
- *
19535
- * `generated-schema-staleness.ts` answers a narrower question (does the file
19536
- * name every table the collections do) from the file's *source text*. This one
19537
- * compares the loaded module against the database, column by column.
19538
- */
19539
- /**
19540
- * The families a boot warning is worth raising over.
19541
- *
19542
- * Deliberately coarse. `varchar(120)` and `text` are the same family because the
19543
- * runtime treats them identically and a warning about the difference would fire
19544
- * on every boot of a correct project — the widths still reach Postgres, which
19545
- * enforces them. What this is looking for is the kind of drift that changes what
19546
- * a value *is*: a number that became text, a timestamp that became a date, a
19547
- * column that stopped existing.
19548
- */
19549
- function typeFamily(sqlType) {
19550
- if (!sqlType) return "unknown";
19551
- let type = sqlType.trim().toLowerCase();
19552
- const isArray = type.endsWith("[]") || type.startsWith("_");
19553
- type = type.replace(/\[\]$/, "").replace(/^_/, "");
19554
- type = type.replace(/\s*\([^)]*\)\s*$/, "").trim();
19555
- const family = FAMILIES[type] ?? type;
19556
- return isArray ? `${family}[]` : family;
19557
- }
19558
- var FAMILIES = {
19559
- text: "text",
19560
- varchar: "text",
19561
- "character varying": "text",
19562
- bpchar: "text",
19563
- char: "text",
19564
- character: "text",
19565
- citext: "text",
19566
- name: "text",
19567
- int2: "integer",
19568
- smallint: "integer",
19569
- smallserial: "integer",
19570
- int4: "integer",
19571
- int: "integer",
19572
- integer: "integer",
19573
- serial: "integer",
19574
- int8: "bigint",
19575
- bigint: "bigint",
19576
- bigserial: "bigint",
19577
- float4: "float",
19578
- real: "float",
19579
- float8: "float",
19580
- "double precision": "float",
19581
- numeric: "numeric",
19582
- decimal: "numeric",
19583
- money: "numeric",
19584
- bool: "boolean",
19585
- boolean: "boolean",
19586
- uuid: "uuid",
19587
- json: "json",
19588
- jsonb: "jsonb",
19589
- date: "date",
19590
- time: "time",
19591
- "time without time zone": "time",
19592
- timetz: "time",
19593
- "time with time zone": "time",
19594
- timestamp: "timestamp",
19595
- "timestamp without time zone": "timestamp",
19596
- timestamptz: "timestamptz",
19597
- "timestamp with time zone": "timestamptz",
19598
- interval: "interval",
19599
- bytea: "bytea",
19600
- tsvector: "tsvector",
19601
- tsquery: "tsquery",
19602
- vector: "vector"
19603
- };
19604
- function catalogueColumns(meta) {
19605
- const columns = /* @__PURE__ */ new Map();
19606
- for (const col of meta.columns) columns.set(col.column_name, {
19607
- name: col.column_name,
19608
- family: typeFamily(col.udt_name || col.data_type),
19609
- notNull: col.is_nullable === "NO"
19610
- });
19611
- return columns;
19612
- }
19613
- /**
19614
- * Every way the generated module and the database disagree, for the tables both
19615
- * describe.
19616
- *
19617
- * One-directional in one respect only: a table the module has and the database
19618
- * does not is reported (that is the stale-file case), while a table the database
19619
- * has and the module does not is left alone — a database may hold anything, and
19620
- * the collections-vs-file question is `generated-schema-staleness`'s.
19621
- */
19622
- function diffGeneratedSchemaAgainstCatalogue(options) {
19623
- const { generated, catalogue, collections } = options;
19624
- if (!generated) return [];
19625
- const collectionByTable = /* @__PURE__ */ new Map();
19626
- for (const collection of collections) {
19627
- const table = getTableName$1(collection);
19628
- if (table) collectionByTable.set(bareTableName(table), collection);
19629
- }
19630
- const differences = [];
19631
- for (const value of Object.values(generated)) {
19632
- if (!isTable(value)) continue;
19633
- const table = getTableName(value);
19634
- const collection = collectionByTable.get(table);
19635
- const at = {
19636
- table,
19637
- collection: collection?.slug
19638
- };
19639
- const meta = catalogue.get(table);
19640
- if (!meta) {
19641
- if (collection) differences.push({
19642
- ...at,
19643
- kind: "missing-table"
19644
- });
19645
- continue;
19646
- }
19647
- const live = catalogueColumns(meta);
19648
- const seen = /* @__PURE__ */ new Set();
19649
- const authOwned = isAuthCollection(collection) ? new Set(AUTH_USERS_COLUMNS.map((c) => c.column)) : void 0;
19650
- for (const [key, column] of Object.entries(getTableColumns(value))) {
19651
- const name = column.name ?? key;
19652
- seen.add(name);
19653
- const property = collection ? fieldKeyForColumn(collection, name) : key;
19654
- const generatedFamily = typeFamily(column.getSQLType?.());
19655
- const generatedNotNull = Boolean(column.notNull);
19656
- const found = live.get(name);
19657
- if (!found) {
19658
- differences.push({
19659
- ...at,
19660
- kind: "missing-column",
19661
- column: name,
19662
- property,
19663
- generated: generatedFamily
19664
- });
19665
- continue;
19666
- }
19667
- if (found.family !== generatedFamily) differences.push({
19668
- ...at,
19669
- kind: "type",
19670
- column: name,
19671
- property,
19672
- generated: generatedFamily,
19673
- live: found.family
19674
- });
19675
- if (found.notNull !== generatedNotNull && !authOwned?.has(name)) differences.push({
19676
- ...at,
19677
- kind: "nullability",
19678
- column: name,
19679
- property,
19680
- generated: generatedNotNull ? "NOT NULL" : "nullable",
19681
- live: found.notNull ? "NOT NULL" : "nullable"
19682
- });
19683
- }
19684
- for (const [name, column] of live) {
19685
- if (seen.has(name)) continue;
19686
- if (authOwned?.has(name)) continue;
19687
- differences.push({
19688
- ...at,
19689
- kind: "extra-column",
19690
- column: name,
19691
- property: collection ? fieldKeyForColumn(collection, name) : name,
19692
- live: column.family
19693
- });
19694
- }
19695
- }
19696
- return differences;
19697
- }
19698
- /** One line, in the terms the reader has: collection, property, column. */
19699
- function describeSchemaDifference(difference) {
19700
- const where = difference.collection ? `${difference.collection} (table "${difference.table}")` : `table "${difference.table}"`;
19701
- const field = difference.column ? `${difference.property && difference.property !== difference.column ? `${difference.property} → "${difference.column}"` : `"${difference.column}"`}` : void 0;
19702
- switch (difference.kind) {
19703
- case "missing-table": return `${where} is in schema.generated.ts but not in the database`;
19704
- case "missing-column": return `${where}: ${field} is in schema.generated.ts but not in the database`;
19705
- case "extra-column": return `${where}: ${field} is in the database but not in schema.generated.ts (${difference.live})`;
19706
- case "type": return `${where}: ${field} is ${difference.generated} in schema.generated.ts and ${difference.live} in the database`;
19707
- case "nullability": return `${where}: ${field} is ${difference.generated} in schema.generated.ts and ${difference.live} in the database`;
19708
- }
19709
- }
19710
- /**
19711
- * Report the differences, one warning each, and never throw.
19712
- *
19713
- * Per difference rather than one summary block on purpose: these are read out of
19714
- * a log aggregator as often as a terminal, and a single multi-line message
19715
- * cannot be filtered down to the one collection somebody is looking at.
19716
- *
19717
- * The fix it names is `rebase db push`. `rebase dev` rewrites
19718
- * schema.generated.ts from the collections on every change, and boot applies
19719
- * only additive changes, so what is still different at boot is the database
19720
- * behind the file: a rename, a dropped column, a narrowed type, a NOT NULL.
19721
- * `rebase schema generate` rewrites the file and leaves the database as it is,
19722
- * so it is named only for collections that changed since the file was written.
19723
- */
19724
- function warnOnGeneratedSchemaDrift(differences) {
19725
- if (differences.length === 0) return;
19726
- logger.warn(`📄 [schema] schema.generated.ts and the database disagree (${differences.length} difference${differences.length === 1 ? "" : "s"}). Boot applies only additive changes; these are the ones it left for you. Run \`rebase db push\` to apply them (it shows the plan and asks before anything destructive), or \`rebase db generate\` for a migration. If the collections changed since schema.generated.ts was written, run \`rebase schema generate\` first.`);
19727
- for (const difference of differences) logger.warn(` • ${describeSchemaDifference(difference)}`, {
19728
- schemaDrift: {
19729
- kind: difference.kind,
19730
- table: difference.table,
19731
- collection: difference.collection,
19732
- column: difference.column,
19733
- property: difference.property,
19734
- generated: difference.generated,
19735
- live: difference.live
19736
- },
19737
- fix: "rebase db push"
19738
- });
19739
- }
19740
- //#endregion
19741
18777
  //#region src/PostgresBootstrapper.ts
19742
18778
  /**
19743
18779
  * PostgresBootstrapper
@@ -19941,6 +18977,18 @@ function diagnoseConnectFailure(err, connectionString) {
19941
18977
  }
19942
18978
  const { fatal, reason, code } = classifyConnectFailure(err);
19943
18979
  const detail = code ? ` [${code}]` : "";
18980
+ const closed = closedDatabaseName(err);
18981
+ if (closed) {
18982
+ logger.error(diagnoseDbError(err, url) ?? `❌ Database "${closed}" at ${hostInfo} is closed to new connections`);
18983
+ return new Error(`Database "${closed}" at ${hostInfo} is closed to new connections: ${reason}${detail}. Open it again with \`rebase db branch list\`.`, { cause: err });
18984
+ }
18985
+ if (code === "53300") {
18986
+ const remedy = extractPgError(err)?.hint ?? "Close another client, cap this process's pool with REBASE_DB_POOL_MAX, or raise the server's max_connections.";
18987
+ logger.error(`
18988
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
18989
+ ❌ PostgreSQL at ${hostInfo} has no free connection\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n ${reason}${detail}\n\n ${remedy}\n\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`);
18990
+ return;
18991
+ }
19944
18992
  if (fatal) {
19945
18993
  logger.error(diagnoseDbError(err, url) ?? `
19946
18994
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
@@ -20147,7 +19195,7 @@ async function openPostgresDriver(pgConfig, config) {
20147
19195
  const { readUrl, directUrl } = resolveSourceUrls(pgConfig, sourceKey, {});
20148
19196
  let readDb;
20149
19197
  if (readUrl && readUrl !== pgConfig.connectionString) try {
20150
- const { createReadReplicaConnection } = await import("./connection-C938nR3E.js").then((n) => n.n);
19198
+ const { createReadReplicaConnection } = await import("./connection-czvI7TZL.js").then((n) => n.n);
20151
19199
  readDb = createReadReplicaConnection(readUrl, mergedSchema).db;
20152
19200
  logger.info("📖 [PostgresBootstrapper] Read replica connection established");
20153
19201
  } catch (err) {
@@ -20260,7 +19308,7 @@ async function openPostgresDriver(pgConfig, config) {
20260
19308
  };
20261
19309
  logger.info(`📡 [CDC] Realtime source = database-level change capture (mode: ${cdcMode === "wal" ? "wal→trigger" : "trigger"}). All writes now emit realtime events regardless of origin.` + (realtimeSubscribes ? "" : " This process installs the capture but does not consume it."));
20262
19310
  } catch (err) {
20263
- if (explicitCdc) logger.warn("⚠️ [CDC] Could not enable database-level change capture — falling back to app-level realtime.", { error: err });
19311
+ if (explicitCdc) logger.warn("⚠️ [CDC] Could not enable database-level change capture — falling back to app-level realtime.", { detail: err instanceof Error ? err.message : String(err) });
20264
19312
  else logger.info("ℹ️ [CDC] Database-level change capture unavailable (likely insufficient privileges to create triggers, or the LISTEN connection was refused) — using app-level realtime. Set REALTIME_CDC=off to silence this.", { detail: err instanceof Error ? err.message : String(err) });
20265
19313
  }
20266
19314
  }
@@ -20276,7 +19324,7 @@ async function openPostgresDriver(pgConfig, config) {
20276
19324
  if (!cdcEnabled && directUrl && realtimeSubscribes) try {
20277
19325
  await realtimeService.startListening(directUrl);
20278
19326
  } catch (err) {
20279
- logger.warn("⚠️ Cross-instance realtime could not be started", { error: err });
19327
+ logger.warn("⚠️ Cross-instance realtime could not be started", { detail: err instanceof Error ? err.message : String(err) });
20280
19328
  }
20281
19329
  else if (!cdcEnabled && !realtimeSubscribes && !realtimeOff) realtimeService.startPublishing();
20282
19330
  try {
@@ -20419,7 +19467,7 @@ function createPostgresBootstrapper(pgConfig) {
20419
19467
  const openIndexBuildSession = (driverResult) => async () => {
20420
19468
  const db = requireProvisioningDb(driverResult);
20421
19469
  const pool = "$client" in db ? db.$client : void 0;
20422
- const { isPgPool, poolIndexBuildSession } = await import("./deferred-index-build-Ca6OqKyR.js").then((n) => n.r);
19470
+ const { isPgPool, poolIndexBuildSession } = await import("./deferred-index-build-BE0I6sYU.js").then((n) => n.r);
20423
19471
  if (isPgPool(pool)) return poolIndexBuildSession(pool)();
20424
19472
  const queryable = provisioningQueryable(driverResult);
20425
19473
  return {
@@ -20445,7 +19493,7 @@ function createPostgresBootstrapper(pgConfig) {
20445
19493
  */
20446
19494
  const provisionRlsRuntime = async (driverResult) => {
20447
19495
  const db = requireProvisioningDb(driverResult);
20448
- const { RLS_BOOTSTRAP_STATEMENTS, rlsRuntimeIsCurrent } = await import("./rls-bootstrap-sql-Bd-hJk08.js").then((n) => n.a);
19496
+ const { RLS_BOOTSTRAP_STATEMENTS, rlsRuntimeIsCurrent } = await import("./db-subcommands-BPoHCxJR.js").then((n) => n.v);
20449
19497
  const run = async (text) => sqlRows(await db.execute(sql.raw(text)));
20450
19498
  if (await rlsRuntimeIsCurrent(run).catch(() => false)) return;
20451
19499
  await db.transaction(async (tx) => {
@@ -20539,11 +19587,11 @@ function createPostgresBootstrapper(pgConfig) {
20539
19587
  unavailable: historyUnavailableReason(state, { requireTransaction: false })
20540
19588
  }) };
20541
19589
  }
20542
- const { BOOT_LOCK_TIMEOUT_MS } = await import("./boot-ddl-Dnvpmm_T.js").then((n) => n.i);
19590
+ const { BOOT_LOCK_TIMEOUT_MS } = await import("./boot-ddl-DYH95Qeg.js").then((n) => n.i);
20543
19591
  const historyTable = await ensureHistoryTableExists(db, { lockTimeoutMs: BOOT_LOCK_TIMEOUT_MS });
20544
19592
  const historyIndexes = await ensureHistoryIndexes(db, { lockTimeoutMs: BOOT_LOCK_TIMEOUT_MS });
20545
19593
  if (historyIndexes.length > 0) {
20546
- const { buildDeferredIndexes, scheduleDeferredIndexBuild } = await import("./deferred-index-build-Ca6OqKyR.js").then((n) => n.r);
19594
+ const { buildDeferredIndexes, scheduleDeferredIndexBuild } = await import("./deferred-index-build-BE0I6sYU.js").then((n) => n.r);
20547
19595
  const indexOptions = {
20548
19596
  log: (message) => logger.info(`[history] ${message}`),
20549
19597
  lockTimeoutMs: BOOT_LOCK_TIMEOUT_MS
@@ -20580,7 +19628,9 @@ function createPostgresBootstrapper(pgConfig) {
20580
19628
  try {
20581
19629
  await db.execute(sql`SELECT 1`);
20582
19630
  } catch (err) {
20583
- throw diagnoseConnectFailure(err, pgConfig.connectionString) ?? new Error(`Cannot reach PostgreSQL at ${parseHostInfo(pgConfig.connectionString ?? "")} to provision the collection schema.`, { cause: err });
19631
+ const hostInfo = parseHostInfo(pgConfig.connectionString ?? "");
19632
+ const { reason, code } = classifyConnectFailure(err);
19633
+ throw diagnoseConnectFailure(err, pgConfig.connectionString) ?? new Error(code === "53300" ? `PostgreSQL at ${hostInfo} has no free connection: ${reason} [53300]` : `Cannot reach PostgreSQL at ${hostInfo} to provision the collection schema.`, { cause: err });
20584
19634
  }
20585
19635
  },
20586
19636
  /**
@@ -20597,9 +19647,9 @@ function createPostgresBootstrapper(pgConfig) {
20597
19647
  * proved it can bootstrap with.
20598
19648
  */
20599
19649
  async ensureCollectionSchema(collections, driverResult, log) {
20600
- const { ensureCollectionTables } = await import("./ensure-collection-tables-Do-Bvb64.js").then((n) => n.n);
19650
+ const { ensureCollectionTables } = await import("./ensure-collection-tables-Cet4650m.js").then((n) => n.n);
20601
19651
  const queryable = provisioningQueryable(driverResult);
20602
- const { BOOT_LOCK_TIMEOUT_MS } = await import("./boot-ddl-Dnvpmm_T.js").then((n) => n.i);
19652
+ const { BOOT_LOCK_TIMEOUT_MS } = await import("./boot-ddl-DYH95Qeg.js").then((n) => n.i);
20603
19653
  const provisionOnly = ["1", "true"].includes((process.env.REBASE_PROVISION_ONLY ?? "").trim().toLowerCase());
20604
19654
  const plan = await ensureCollectionTables(queryable, collections, log, {
20605
19655
  lockTimeoutMs: BOOT_LOCK_TIMEOUT_MS,
@@ -20611,7 +19661,7 @@ function createPostgresBootstrapper(pgConfig) {
20611
19661
  pendingIndexBuild?.cancel();
20612
19662
  pendingIndexBuild = void 0;
20613
19663
  if (!provisionOnly && plan.deferredIndexes.length > 0) {
20614
- const { scheduleDeferredIndexBuild } = await import("./deferred-index-build-Ca6OqKyR.js").then((n) => n.r);
19664
+ const { scheduleDeferredIndexBuild } = await import("./deferred-index-build-BE0I6sYU.js").then((n) => n.r);
20615
19665
  logger.info(`[schema] ${plan.deferredIndexes.length} index(es) are missing, or were left INVALID, on tables that already exist. They are built in the background once the server is serving, one at a time, without blocking reads or writes.`);
20616
19666
  pendingIndexBuild = scheduleDeferredIndexBuild(openIndexBuildSession(driverResult), plan.deferredIndexes, {
20617
19667
  log: (message) => logger.info(`[schema] ${message}`),
@@ -20651,7 +19701,7 @@ function createPostgresBootstrapper(pgConfig) {
20651
19701
  await provisionRlsRuntime(driverResult);
20652
19702
  },
20653
19703
  async ensureCollectionPolicies(collections, driverResult, log) {
20654
- const { ensureCollectionPolicies, describeRulesDropped } = await import("./ensure-collection-policies-C1ux3V_z.js");
19704
+ const { ensureCollectionPolicies, describeRulesDropped } = await import("./ensure-collection-policies-BnGOURpG.js");
20655
19705
  const queryable = provisioningQueryable(driverResult);
20656
19706
  const { rows } = await queryable.query("SELECT to_regprocedure('rebase.roles()') IS NOT NULL AS present");
20657
19707
  if (!rows[0]?.present) await provisionRlsRuntime(driverResult);
@@ -20676,7 +19726,7 @@ function createPostgresBootstrapper(pgConfig) {
20676
19726
  for (const u of outcome.unsecured.filter((u) => u.grantWithdrawn)) logger.error(`🔐 [rls] Could not enable row-level security on "${u.table}": ${u.error}. Its privileges have been revoked from ${REBASE_USER_ROLE$1}, so the table is unreachable rather than unprotected. Reads and writes to that collection will fail until RLS can be enabled.`);
20677
19727
  if (unrevoked.length > 0) throw new Error("Refusing to start: row-level security could not be enabled on " + unrevoked.map((u) => `"${u.table}" (${u.error})`).join(", ") + `, and the privileges granted to ${REBASE_USER_ROLE$1} could not be revoked either. The table would be served with no row filtering. Fix the database permissions (the connection role must own these tables, or be able to ALTER them) and boot again.`);
20678
19728
  try {
20679
- const { dropLegacyAuthSchema } = await import("./rls-bootstrap-sql-Bd-hJk08.js").then((n) => n.a);
19729
+ const { dropLegacyAuthSchema } = await import("./db-subcommands-BPoHCxJR.js").then((n) => n.v);
20680
19730
  await dropLegacyAuthSchema(async (text) => (await queryable.query(text)).rows, {
20681
19731
  info: (m) => logger.info(m),
20682
19732
  warn: (m) => logger.warn(m)
@@ -20767,7 +19817,7 @@ function createPostgresBootstrapper(pgConfig) {
20767
19817
  return driverResult.internals.driver.admin;
20768
19818
  },
20769
19819
  async initializeWebsockets(server, realtimeService, driver, config, adapter, limits) {
20770
- const { createPostgresWebSocket, isSocketRealtimeService, socketRealtimeMissing } = await import("./websocket-DMeXOoyv.js").then((n) => n.c);
19820
+ const { createPostgresWebSocket, isSocketRealtimeService, socketRealtimeMissing } = await import("./websocket-BKZwsjmf.js").then((n) => n.c);
20771
19821
  if (!isSocketRealtimeService(realtimeService)) throw new Error(`The realtime service handed to the Postgres socket cannot serve it — it has no ${socketRealtimeMissing(realtimeService).join(", ")}. Every socket would fail on its first frame.`);
20772
19822
  const realtimeConfig = effectiveConfig().realtime;
20773
19823
  const maxSocketsPerUser = realtimeConfig?.maxSocketsPerUser !== void 0 ? parseMaxSocketsPerUser(realtimeConfig.maxSocketsPerUser, "realtime.maxSocketsPerUser") : void 0;
@@ -20838,7 +19888,8 @@ async function openRebase(options) {
20838
19888
  await pool?.query("SELECT 1");
20839
19889
  } catch (error) {
20840
19890
  await owned?.pool.end().catch(() => void 0);
20841
- throw new Error(`Cannot connect to PostgreSQL at ${parseHostInfo(connectionString ?? "")}: ${deepestErrorMessage(error) ?? "the connection failed"}.`, { cause: error });
19891
+ const hint = extractPgError(error)?.hint;
19892
+ throw new Error(`Cannot connect to PostgreSQL at ${parseHostInfo(connectionString ?? "")}: ${deepestErrorMessage(error) ?? "the connection failed"}.${hint ? ` ${hint}` : ""}`, { cause: error });
20842
19893
  }
20843
19894
  let opened;
20844
19895
  try {
@@ -20905,4 +19956,4 @@ async function openRebase(options) {
20905
19956
  //#endregion
20906
19957
  export { AuthenticatedPostgresBackendDriver as $, budgetSendBuffers as A, mfaFactors as B, PG_NOTIFY_MAX_PAYLOAD_BYTES as C, MemoryChannelBus as D, parseChannelBusPayload as E, createAuthSchema as F, recoveryCodesRelations as G, passwordResetTokens as H, magicLinkTokens as I, userIdentities as J, refreshTokens as K, magicLinkTokensRelations as L, terminateIfBacklogged as M, assertReadRequestReadable as N, frameByteLength as O, appConfig as P, usersSchema as Q, mfaChallenges as R, DEFAULT_BATCH_WINDOW_MS as S, parseChannelBusFrame as T, passwordResetTokensRelations as U, mfaFactorsRelations as V, recoveryCodes as W, users as X, userIdentitiesRelations as Y, usersRelations as Z, parseRealtimeTuning as _, listedColumnKeys as _t, openPostgresDriver as a, isRoleSwitchingOptedOut as at, resolveChannelBusSetting as b, resolveSourceUrls as c, SqlResultTooLargeError as ct, DEFAULT_RECOUNT_MIN_ROWS as d, DrizzleConditionBuilder as dt, CONNECTION_OWNER as et, PostgresRealtimeProvider as f, PostgresConditionBuilder as ft, parseMaxSubscriptionsPerSocket as g, joinColumn as gt, parseMaxSocketsPerUser as h, getUnknownFilterFieldsMode as ht, isScaffoldedLocalDatabase as i, effectiveSqlRole as it, reapSilentSockets as j, isChannelBusInstance$1 as k, PostgresCollectionRegistry as l, isNestedPath as lt, packEntityChanges as m, escapeLikePattern as mt, createPostgresBootstrapper as n, RoleSwitchUnavailableError as nt, postgresDriverConfigFromEnv as o, sqlRoleRanAs as ot, RealtimeService as p, configureUnknownFilterFields as pt, refreshTokensRelations as q, describeSchemaDriftCause as r, branchParentDatabase as rt, resolveDriftCheckName as s, SQL_RESULT_BYTE_BUDGET as st, openRebase as t, PostgresBackendDriver as tt, DEFAULT_MAX_SUBSCRIPTIONS_PER_SOCKET as u, resolveNestedPath as ut, refetchConcurrencyFor as v, PostgresChannelBus as w, CHANNEL_BUS_NOTIFY_CHANNEL as x, createChannelBus as y, mfaChallengesRelations as z };
20907
19958
 
20908
- //# sourceMappingURL=open-rebase-D8rytTP0.js.map
19959
+ //# sourceMappingURL=open-rebase-EmqVmS14.js.map