@rebasepro/server-postgresql 0.0.1-canary.09e5ec5

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 (196) hide show
  1. package/LICENSE +6 -0
  2. package/README.md +106 -0
  3. package/build-errors.txt +37 -0
  4. package/dist/common/src/collections/CollectionRegistry.d.ts +56 -0
  5. package/dist/common/src/collections/index.d.ts +1 -0
  6. package/dist/common/src/data/buildRebaseData.d.ts +14 -0
  7. package/dist/common/src/index.d.ts +3 -0
  8. package/dist/common/src/util/builders.d.ts +57 -0
  9. package/dist/common/src/util/callbacks.d.ts +6 -0
  10. package/dist/common/src/util/collections.d.ts +11 -0
  11. package/dist/common/src/util/common.d.ts +2 -0
  12. package/dist/common/src/util/conditions.d.ts +26 -0
  13. package/dist/common/src/util/entities.d.ts +58 -0
  14. package/dist/common/src/util/enums.d.ts +3 -0
  15. package/dist/common/src/util/index.d.ts +16 -0
  16. package/dist/common/src/util/navigation_from_path.d.ts +34 -0
  17. package/dist/common/src/util/navigation_utils.d.ts +20 -0
  18. package/dist/common/src/util/parent_references_from_path.d.ts +6 -0
  19. package/dist/common/src/util/paths.d.ts +14 -0
  20. package/dist/common/src/util/permissions.d.ts +5 -0
  21. package/dist/common/src/util/references.d.ts +2 -0
  22. package/dist/common/src/util/relations.d.ts +22 -0
  23. package/dist/common/src/util/resolutions.d.ts +72 -0
  24. package/dist/common/src/util/storage.d.ts +24 -0
  25. package/dist/index.es.js +11298 -0
  26. package/dist/index.es.js.map +1 -0
  27. package/dist/index.umd.js +11306 -0
  28. package/dist/index.umd.js.map +1 -0
  29. package/dist/server-postgresql/src/PostgresBackendDriver.d.ts +100 -0
  30. package/dist/server-postgresql/src/PostgresBootstrapper.d.ts +40 -0
  31. package/dist/server-postgresql/src/auth/ensure-tables.d.ts +6 -0
  32. package/dist/server-postgresql/src/auth/services.d.ts +192 -0
  33. package/dist/server-postgresql/src/cli.d.ts +1 -0
  34. package/dist/server-postgresql/src/collections/PostgresCollectionRegistry.d.ts +43 -0
  35. package/dist/server-postgresql/src/connection.d.ts +40 -0
  36. package/dist/server-postgresql/src/data-transformer.d.ts +58 -0
  37. package/dist/server-postgresql/src/databasePoolManager.d.ts +20 -0
  38. package/dist/server-postgresql/src/history/HistoryService.d.ts +71 -0
  39. package/dist/server-postgresql/src/history/ensure-history-table.d.ts +7 -0
  40. package/dist/server-postgresql/src/index.d.ts +13 -0
  41. package/dist/server-postgresql/src/interfaces.d.ts +18 -0
  42. package/dist/server-postgresql/src/schema/auth-schema.d.ts +868 -0
  43. package/dist/server-postgresql/src/schema/doctor-cli.d.ts +2 -0
  44. package/dist/server-postgresql/src/schema/doctor.d.ts +43 -0
  45. package/dist/server-postgresql/src/schema/generate-drizzle-schema-logic.d.ts +2 -0
  46. package/dist/server-postgresql/src/schema/generate-drizzle-schema.d.ts +1 -0
  47. package/dist/server-postgresql/src/schema/introspect-db-logic.d.ts +82 -0
  48. package/dist/server-postgresql/src/schema/introspect-db.d.ts +1 -0
  49. package/dist/server-postgresql/src/schema/test-schema.d.ts +24 -0
  50. package/dist/server-postgresql/src/services/BranchService.d.ts +47 -0
  51. package/dist/server-postgresql/src/services/EntityFetchService.d.ts +209 -0
  52. package/dist/server-postgresql/src/services/EntityPersistService.d.ts +41 -0
  53. package/dist/server-postgresql/src/services/RelationService.d.ts +98 -0
  54. package/dist/server-postgresql/src/services/entity-helpers.d.ts +38 -0
  55. package/dist/server-postgresql/src/services/entityService.d.ts +104 -0
  56. package/dist/server-postgresql/src/services/index.d.ts +4 -0
  57. package/dist/server-postgresql/src/services/realtimeService.d.ts +188 -0
  58. package/dist/server-postgresql/src/utils/drizzle-conditions.d.ts +116 -0
  59. package/dist/server-postgresql/src/websocket.d.ts +5 -0
  60. package/dist/types/src/controllers/analytics_controller.d.ts +7 -0
  61. package/dist/types/src/controllers/auth.d.ts +119 -0
  62. package/dist/types/src/controllers/client.d.ts +170 -0
  63. package/dist/types/src/controllers/collection_registry.d.ts +45 -0
  64. package/dist/types/src/controllers/customization_controller.d.ts +60 -0
  65. package/dist/types/src/controllers/data.d.ts +168 -0
  66. package/dist/types/src/controllers/data_driver.d.ts +160 -0
  67. package/dist/types/src/controllers/database_admin.d.ts +11 -0
  68. package/dist/types/src/controllers/dialogs_controller.d.ts +36 -0
  69. package/dist/types/src/controllers/effective_role.d.ts +4 -0
  70. package/dist/types/src/controllers/email.d.ts +34 -0
  71. package/dist/types/src/controllers/index.d.ts +18 -0
  72. package/dist/types/src/controllers/local_config_persistence.d.ts +20 -0
  73. package/dist/types/src/controllers/navigation.d.ts +213 -0
  74. package/dist/types/src/controllers/registry.d.ts +54 -0
  75. package/dist/types/src/controllers/side_dialogs_controller.d.ts +67 -0
  76. package/dist/types/src/controllers/side_entity_controller.d.ts +90 -0
  77. package/dist/types/src/controllers/snackbar.d.ts +24 -0
  78. package/dist/types/src/controllers/storage.d.ts +171 -0
  79. package/dist/types/src/index.d.ts +4 -0
  80. package/dist/types/src/rebase_context.d.ts +105 -0
  81. package/dist/types/src/types/backend.d.ts +536 -0
  82. package/dist/types/src/types/builders.d.ts +15 -0
  83. package/dist/types/src/types/chips.d.ts +5 -0
  84. package/dist/types/src/types/collections.d.ts +856 -0
  85. package/dist/types/src/types/cron.d.ts +102 -0
  86. package/dist/types/src/types/data_source.d.ts +64 -0
  87. package/dist/types/src/types/entities.d.ts +145 -0
  88. package/dist/types/src/types/entity_actions.d.ts +98 -0
  89. package/dist/types/src/types/entity_callbacks.d.ts +173 -0
  90. package/dist/types/src/types/entity_link_builder.d.ts +7 -0
  91. package/dist/types/src/types/entity_overrides.d.ts +10 -0
  92. package/dist/types/src/types/entity_views.d.ts +61 -0
  93. package/dist/types/src/types/export_import.d.ts +21 -0
  94. package/dist/types/src/types/index.d.ts +23 -0
  95. package/dist/types/src/types/locales.d.ts +4 -0
  96. package/dist/types/src/types/modify_collections.d.ts +5 -0
  97. package/dist/types/src/types/plugins.d.ts +279 -0
  98. package/dist/types/src/types/properties.d.ts +1176 -0
  99. package/dist/types/src/types/property_config.d.ts +70 -0
  100. package/dist/types/src/types/relations.d.ts +336 -0
  101. package/dist/types/src/types/slots.d.ts +252 -0
  102. package/dist/types/src/types/translations.d.ts +870 -0
  103. package/dist/types/src/types/user_management_delegate.d.ts +121 -0
  104. package/dist/types/src/types/websockets.d.ts +78 -0
  105. package/dist/types/src/users/index.d.ts +2 -0
  106. package/dist/types/src/users/roles.d.ts +22 -0
  107. package/dist/types/src/users/user.d.ts +46 -0
  108. package/drizzle-test/0000_woozy_junta.sql +6 -0
  109. package/drizzle-test/0001_youthful_arachne.sql +1 -0
  110. package/drizzle-test/0002_lively_dragon_lord.sql +2 -0
  111. package/drizzle-test/0003_mean_king_cobra.sql +2 -0
  112. package/drizzle-test/meta/0000_snapshot.json +47 -0
  113. package/drizzle-test/meta/0001_snapshot.json +48 -0
  114. package/drizzle-test/meta/0002_snapshot.json +38 -0
  115. package/drizzle-test/meta/0003_snapshot.json +48 -0
  116. package/drizzle-test/meta/_journal.json +34 -0
  117. package/drizzle-test-out/0000_tan_trauma.sql +6 -0
  118. package/drizzle-test-out/0001_rapid_drax.sql +1 -0
  119. package/drizzle-test-out/meta/0000_snapshot.json +44 -0
  120. package/drizzle-test-out/meta/0001_snapshot.json +54 -0
  121. package/drizzle-test-out/meta/_journal.json +20 -0
  122. package/drizzle.test.config.ts +10 -0
  123. package/jest-all.log +3128 -0
  124. package/jest.log +49 -0
  125. package/package.json +92 -0
  126. package/scratch.ts +41 -0
  127. package/src/PostgresBackendDriver.ts +1008 -0
  128. package/src/PostgresBootstrapper.ts +231 -0
  129. package/src/auth/ensure-tables.ts +381 -0
  130. package/src/auth/services.ts +799 -0
  131. package/src/cli.ts +648 -0
  132. package/src/collections/PostgresCollectionRegistry.ts +96 -0
  133. package/src/connection.ts +84 -0
  134. package/src/data-transformer.ts +608 -0
  135. package/src/databasePoolManager.ts +85 -0
  136. package/src/history/HistoryService.ts +248 -0
  137. package/src/history/ensure-history-table.ts +45 -0
  138. package/src/index.ts +13 -0
  139. package/src/interfaces.ts +60 -0
  140. package/src/schema/auth-schema.ts +169 -0
  141. package/src/schema/doctor-cli.ts +47 -0
  142. package/src/schema/doctor.ts +595 -0
  143. package/src/schema/generate-drizzle-schema-logic.ts +765 -0
  144. package/src/schema/generate-drizzle-schema.ts +151 -0
  145. package/src/schema/introspect-db-logic.ts +542 -0
  146. package/src/schema/introspect-db.ts +211 -0
  147. package/src/schema/test-schema.ts +11 -0
  148. package/src/services/BranchService.ts +237 -0
  149. package/src/services/EntityFetchService.ts +1576 -0
  150. package/src/services/EntityPersistService.ts +349 -0
  151. package/src/services/RelationService.ts +1274 -0
  152. package/src/services/entity-helpers.ts +147 -0
  153. package/src/services/entityService.ts +211 -0
  154. package/src/services/index.ts +13 -0
  155. package/src/services/realtimeService.ts +1034 -0
  156. package/src/utils/drizzle-conditions.ts +1000 -0
  157. package/src/websocket.ts +518 -0
  158. package/test/auth-services.test.ts +661 -0
  159. package/test/batch-many-to-many-regression.test.ts +573 -0
  160. package/test/branchService.test.ts +367 -0
  161. package/test/data-transformer-hardening.test.ts +417 -0
  162. package/test/data-transformer.test.ts +175 -0
  163. package/test/doctor.test.ts +182 -0
  164. package/test/drizzle-conditions.test.ts +895 -0
  165. package/test/entityService.errors.test.ts +367 -0
  166. package/test/entityService.relations.test.ts +1008 -0
  167. package/test/entityService.subcollection-search.test.ts +566 -0
  168. package/test/entityService.test.ts +1035 -0
  169. package/test/generate-drizzle-schema.test.ts +988 -0
  170. package/test/historyService.test.ts +141 -0
  171. package/test/introspect-db-generation.test.ts +436 -0
  172. package/test/introspect-db-utils.test.ts +389 -0
  173. package/test/n-plus-one-regression.test.ts +314 -0
  174. package/test/postgresDataDriver.test.ts +648 -0
  175. package/test/realtimeService.test.ts +307 -0
  176. package/test/relation-pipeline-gaps.test.ts +637 -0
  177. package/test/relations.test.ts +1115 -0
  178. package/test/unmapped-tables-safety.test.ts +345 -0
  179. package/test-drizzle-bug.ts +18 -0
  180. package/test-drizzle-out/0000_cultured_freak.sql +7 -0
  181. package/test-drizzle-out/0001_tiresome_professor_monster.sql +1 -0
  182. package/test-drizzle-out/meta/0000_snapshot.json +55 -0
  183. package/test-drizzle-out/meta/0001_snapshot.json +63 -0
  184. package/test-drizzle-out/meta/_journal.json +20 -0
  185. package/test-drizzle-prompt.sh +2 -0
  186. package/test-policy-prompt.sh +3 -0
  187. package/test-programmatic.ts +30 -0
  188. package/test-programmatic2.ts +59 -0
  189. package/test-schema-no-policies.ts +12 -0
  190. package/test_drizzle_mock.js +3 -0
  191. package/test_find_changed.mjs +32 -0
  192. package/test_hash.js +14 -0
  193. package/test_output.txt +3145 -0
  194. package/tsconfig.json +49 -0
  195. package/tsconfig.prod.json +20 -0
  196. package/vite.config.ts +82 -0
@@ -0,0 +1,211 @@
1
+ import chalk from "chalk";
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import pg from "pg";
5
+ import arg from "arg";
6
+ import * as dotenv from "dotenv";
7
+
8
+ import {
9
+ TableRow,
10
+ TableColumn,
11
+ EnumValue,
12
+ PrimaryKeyRow,
13
+ ForeignKeyRow,
14
+ buildTablesMap,
15
+ buildEnumMap,
16
+ identifyJoinTables,
17
+ generateCollectionFile,
18
+ generateIndexContent,
19
+ mergeIndexContent,
20
+ safeHostFromUrl,
21
+ } from "./introspect-db-logic";
22
+
23
+ async function main() {
24
+ const args = arg(
25
+ {
26
+ "--output": String,
27
+ "--force": Boolean,
28
+ "--schema": String,
29
+ "-o": "--output",
30
+ "-f": "--force",
31
+ },
32
+ { permissive: true }
33
+ );
34
+
35
+ const outDir = args["--output"] || path.resolve(process.cwd(), "config", "collections");
36
+ const force = args["--force"] || false;
37
+ const pgSchema = args["--schema"] || "public";
38
+
39
+ if (!fs.existsSync(outDir)) {
40
+ fs.mkdirSync(outDir, { recursive: true });
41
+ }
42
+
43
+ // Load env
44
+ const envPaths = [
45
+ process.env.DOTENV_CONFIG_PATH,
46
+ path.resolve(process.cwd(), ".env"),
47
+ path.resolve(process.cwd(), "../.env"),
48
+ path.resolve(process.cwd(), "../../.env")
49
+ ].filter(Boolean) as string[];
50
+
51
+ for (const p of envPaths) {
52
+ if (fs.existsSync(p)) {
53
+ dotenv.config({ path: p });
54
+ break;
55
+ }
56
+ }
57
+
58
+ const databaseUrl = process.env.DATABASE_URL || process.env.ADMIN_CONNECTION_STRING;
59
+ if (!databaseUrl) {
60
+ console.error(chalk.red("✗ DATABASE_URL is not set. Make sure your .env file is configured."));
61
+ process.exit(1);
62
+ }
63
+
64
+ const client = new pg.Client({ connectionString: databaseUrl });
65
+
66
+ try {
67
+ await client.connect();
68
+ } catch (err) {
69
+ console.error(chalk.red(`✗ Failed to connect to database: ${err instanceof Error ? err.message : String(err)}`));
70
+ console.error(chalk.gray(" Check your DATABASE_URL and ensure the database is reachable."));
71
+ process.exit(1);
72
+ }
73
+
74
+ // Log the host portion safely — handle URLs without "@"
75
+ const hostPart = safeHostFromUrl(databaseUrl);
76
+ console.log(chalk.gray(`Connected to database: ${hostPart}`));
77
+ console.log(chalk.gray(`Introspecting schema '${pgSchema}'...`));
78
+
79
+ try {
80
+ // 1. Get Tables
81
+ const { rows: tables } = await client.query<TableRow>(`
82
+ SELECT table_name
83
+ FROM information_schema.tables
84
+ WHERE table_schema = $1 AND table_type = 'BASE TABLE'
85
+ AND table_name NOT LIKE 'drizzle_%'
86
+ AND table_name NOT LIKE 'rebase_%'
87
+ ORDER BY table_name
88
+ `, [pgSchema]);
89
+
90
+ // 2. Get Columns
91
+ const { rows: columns } = await client.query<TableColumn>(`
92
+ SELECT table_name, column_name, data_type, udt_name, is_nullable, column_default
93
+ FROM information_schema.columns
94
+ WHERE table_schema = $1
95
+ `, [pgSchema]);
96
+
97
+ // 2b. Get Enum Types and their values
98
+ const { rows: enumValues } = await client.query<EnumValue>(`
99
+ SELECT t.typname AS enum_name,
100
+ e.enumlabel AS enum_value,
101
+ e.enumsortorder AS sort_order
102
+ FROM pg_type t
103
+ JOIN pg_enum e ON t.oid = e.enumtypid
104
+ JOIN pg_namespace n ON t.typnamespace = n.oid
105
+ WHERE n.nspname = $1
106
+ ORDER BY t.typname, e.enumsortorder
107
+ `, [pgSchema]);
108
+
109
+ // Build a map: enum_name -> ordered list of values
110
+ const enumMap = buildEnumMap(enumValues);
111
+
112
+ // 3. Get Primary Keys
113
+ const { rows: pks } = await client.query<PrimaryKeyRow>(`
114
+ SELECT t.relname as table_name, a.attname as column_name
115
+ FROM pg_index i
116
+ JOIN pg_attribute a ON a.attrelid = i.indrelid
117
+ AND a.attnum = ANY(i.indkey)
118
+ JOIN pg_class t ON t.oid = i.indrelid
119
+ JOIN pg_namespace n ON n.oid = t.relnamespace
120
+ WHERE i.indisprimary AND n.nspname = $1
121
+ `, [pgSchema]);
122
+
123
+ // 4. Get Foreign Keys
124
+ const { rows: fks } = await client.query<ForeignKeyRow>(`
125
+ SELECT
126
+ tc.table_name,
127
+ kcu.column_name,
128
+ ccu.table_name AS foreign_table_name,
129
+ ccu.column_name AS foreign_column_name
130
+ FROM
131
+ information_schema.table_constraints AS tc
132
+ JOIN information_schema.key_column_usage AS kcu
133
+ ON tc.constraint_name = kcu.constraint_name
134
+ AND tc.table_schema = kcu.table_schema
135
+ JOIN information_schema.constraint_column_usage AS ccu
136
+ ON ccu.constraint_name = tc.constraint_name
137
+ AND ccu.table_schema = tc.table_schema
138
+ WHERE tc.constraint_type = 'FOREIGN KEY' AND tc.table_schema = $1
139
+ `, [pgSchema]);
140
+
141
+ const tablesMap = buildTablesMap(tables, columns, pks, fks);
142
+ const joinTables = identifyJoinTables(tablesMap);
143
+
144
+ console.log(chalk.blue(`Found ${tablesMap.size} tables (including ${joinTables.size} detected join tables).`));
145
+
146
+ // Generate Collections
147
+ const generatedFiles: string[] = [];
148
+ const skippedFiles: string[] = [];
149
+
150
+ for (const [tableName, meta] of tablesMap.entries()) {
151
+ if (joinTables.has(tableName)) continue; // We don't generate base collections for pure join tables
152
+
153
+ // ── File overwrite protection ──────────────────────────────
154
+ const filePath = path.join(outDir, `${tableName}.ts`);
155
+ if (fs.existsSync(filePath) && !force) {
156
+ skippedFiles.push(tableName);
157
+ continue;
158
+ }
159
+
160
+ const fileContent = generateCollectionFile(
161
+ tableName,
162
+ meta,
163
+ fks,
164
+ joinTables,
165
+ tablesMap,
166
+ enumMap,
167
+ );
168
+
169
+ fs.writeFileSync(filePath, fileContent, "utf-8");
170
+ generatedFiles.push(tableName);
171
+ console.log(chalk.green(` ✓ ${filePath}`));
172
+ }
173
+
174
+ // Generate index.ts (sorted alphabetically for deterministic output)
175
+ if (generatedFiles.length > 0) {
176
+ const indexPath = path.join(outDir, "index.ts");
177
+
178
+ if (fs.existsSync(indexPath) && !force) {
179
+ // Merge: read existing index, add new exports that don't already exist
180
+ const existing = fs.readFileSync(indexPath, "utf-8");
181
+ const merged = mergeIndexContent(existing, generatedFiles);
182
+ fs.writeFileSync(indexPath, merged, "utf-8");
183
+ } else {
184
+ const indexContent = generateIndexContent(generatedFiles);
185
+ fs.writeFileSync(indexPath, indexContent, "utf-8");
186
+ }
187
+ console.log(chalk.green(` ✓ ${indexPath}`));
188
+ }
189
+
190
+ console.log("");
191
+ if (skippedFiles.length > 0) {
192
+ console.log(chalk.yellow(`⚠ Skipped ${skippedFiles.length} existing file(s): ${skippedFiles.join(", ")}`));
193
+ console.log(chalk.gray(` Use --force to overwrite existing files.`));
194
+ console.log("");
195
+ }
196
+ console.log(chalk.bold.green(`✓ Introspected ${tablesMap.size} tables — generated ${generatedFiles.length} collection(s).`));
197
+ console.log(chalk.gray(` Review the generated files in ${outDir} and customize properties as needed.`));
198
+ console.log("");
199
+
200
+ } catch (e) {
201
+ console.error(chalk.red(`✗ Error introspecting database: ${e instanceof Error ? e.message : String(e)}`));
202
+ process.exit(1);
203
+ } finally {
204
+ await client.end();
205
+ }
206
+ }
207
+
208
+ main().catch((err) => {
209
+ console.error(err);
210
+ process.exit(1);
211
+ });
@@ -0,0 +1,11 @@
1
+ import { pgTable, text, pgPolicy } from "drizzle-orm/pg-core";
2
+ import { sql } from "drizzle-orm";
3
+
4
+ export const testTable = pgTable("test", {
5
+ id: text("id").primaryKey()
6
+ }, (t) => [
7
+ pgPolicy("renamed_policy", { as: "permissive",
8
+ to: "public",
9
+ for: "select",
10
+ using: sql`true` })
11
+ ]);
@@ -0,0 +1,237 @@
1
+ /**
2
+ * BranchService
3
+ *
4
+ * Manages database branching by creating/deleting PostgreSQL databases
5
+ * using `CREATE DATABASE ... TEMPLATE`. Branch metadata is stored in the
6
+ * `rebase.branches` table in the default (main) database, following the
7
+ * same `rebase` schema convention used by entity_history, auth, etc.
8
+ */
9
+
10
+ import { sql } from "drizzle-orm";
11
+ import { BranchInfo } from "@rebasepro/types";
12
+ import { DrizzleClient } from "../interfaces";
13
+ import { DatabasePoolManager } from "../databasePoolManager";
14
+
15
+ /** Internal prefix applied to branch database names to avoid collisions. */
16
+ const BRANCH_DB_PREFIX = "rb_";
17
+
18
+ /** Fully-qualified metadata table in the rebase schema. */
19
+ const BRANCHES_TABLE = "rebase.branches";
20
+
21
+ /**
22
+ * Sanitize a user-provided branch name to a safe PostgreSQL identifier.
23
+ * Only allows alphanumeric characters and underscores.
24
+ */
25
+ function sanitizeBranchName(name: string): string {
26
+ return name.replace(/[^a-zA-Z0-9_]/g, "");
27
+ }
28
+
29
+ /**
30
+ * Convert a user-facing branch name to the actual PostgreSQL database name.
31
+ */
32
+ function toBranchDbName(name: string): string {
33
+ const sanitized = sanitizeBranchName(name);
34
+ if (!sanitized) throw new Error("Branch name must contain at least one alphanumeric character.");
35
+ return `${BRANCH_DB_PREFIX}${sanitized}`;
36
+ }
37
+
38
+ export class BranchService {
39
+ constructor(
40
+ private db: DrizzleClient,
41
+ private poolManager: DatabasePoolManager
42
+ ) {}
43
+
44
+ /**
45
+ * Ensure the `rebase.branches` metadata table exists in the default database.
46
+ * Idempotent — safe to call on every startup.
47
+ */
48
+ async ensureBranchMetadataTable(): Promise<void> {
49
+ // Create the rebase schema (idempotent — may already exist from auth/history init)
50
+ await this.db.execute(sql`CREATE SCHEMA IF NOT EXISTS rebase`);
51
+
52
+ await this.db.execute(sql.raw(`
53
+ CREATE TABLE IF NOT EXISTS ${BRANCHES_TABLE} (
54
+ name TEXT PRIMARY KEY,
55
+ db_name TEXT NOT NULL UNIQUE,
56
+ parent_db TEXT NOT NULL,
57
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
58
+ metadata JSONB DEFAULT '{}'
59
+ );
60
+ `));
61
+ }
62
+
63
+ /**
64
+ * Create a new branch database by templating the source database.
65
+ *
66
+ * Uses `CREATE DATABASE ... TEMPLATE` for an instant, full-fidelity copy
67
+ * of both schema and data.
68
+ *
69
+ * @param name User-facing branch name (e.g., "feature_auth")
70
+ * @param options.source Source database to clone; defaults to the main database.
71
+ */
72
+ async createBranch(name: string, options?: { source?: string }): Promise<BranchInfo> {
73
+ const dbName = toBranchDbName(name);
74
+ const sanitizedName = sanitizeBranchName(name);
75
+ const sourceDb = options?.source || this.poolManager.defaultDatabaseName;
76
+
77
+ // Check if branch already exists
78
+ const existing = await this.db.execute(
79
+ sql`SELECT name FROM rebase.branches WHERE name = ${sanitizedName} OR db_name = ${dbName}`
80
+ );
81
+ if ((existing.rows as unknown[]).length > 0) {
82
+ throw new Error(`Branch "${sanitizedName}" already exists.`);
83
+ }
84
+
85
+ // Disconnect any idle pools to the source DB so TEMPLATE works.
86
+ // CREATE DATABASE ... TEMPLATE requires no other connections to the template.
87
+ await this.poolManager.disconnectDatabase(sourceDb);
88
+
89
+ // Create the database using the source as a template.
90
+ // Note: Identifiers must be double-quoted, not parameterized.
91
+ const safeDbName = dbName.replace(/"/g, '""');
92
+ const safeSourceDb = sourceDb.replace(/"/g, '""');
93
+ try {
94
+ await this.db.execute(
95
+ sql.raw(`CREATE DATABASE "${safeDbName}" TEMPLATE "${safeSourceDb}"`)
96
+ );
97
+ } catch (err) {
98
+ const msg = err instanceof Error ? err.message : String(err);
99
+ if (msg.includes("already exists")) {
100
+ throw new Error(`Database "${dbName}" already exists on the server. Choose a different branch name.`);
101
+ }
102
+ // If template fails due to active connections, provide a helpful error
103
+ if (msg.includes("being accessed by other users")) {
104
+ throw new Error(
105
+ `Cannot create branch: the source database "${sourceDb}" has active connections. ` +
106
+ "Close other clients or connections and try again."
107
+ );
108
+ }
109
+ throw err;
110
+ }
111
+
112
+ // Record metadata in the default database
113
+ const now = new Date();
114
+ await this.db.execute(
115
+ sql`INSERT INTO rebase.branches (name, db_name, parent_db, created_at)
116
+ VALUES (${sanitizedName}, ${dbName}, ${sourceDb}, ${now.toISOString()})`
117
+ );
118
+
119
+ return {
120
+ name: sanitizedName,
121
+ parentDatabase: sourceDb,
122
+ createdAt: now
123
+ };
124
+ }
125
+
126
+ /**
127
+ * Delete a branch database and remove its metadata.
128
+ * Cannot delete the main/default database.
129
+ */
130
+ async deleteBranch(name: string): Promise<void> {
131
+ const sanitizedName = sanitizeBranchName(name);
132
+ const dbName = toBranchDbName(name);
133
+
134
+ // Safety: never delete the default database
135
+ if (dbName === this.poolManager.defaultDatabaseName) {
136
+ throw new Error("Cannot delete the main database.");
137
+ }
138
+
139
+ // Verify the branch exists in our metadata
140
+ const existing = await this.db.execute(
141
+ sql`SELECT db_name FROM rebase.branches WHERE name = ${sanitizedName}`
142
+ );
143
+ if ((existing.rows as unknown[]).length === 0) {
144
+ throw new Error(`Branch "${sanitizedName}" not found.`);
145
+ }
146
+
147
+ // Disconnect any pools to this branch before dropping
148
+ await this.poolManager.disconnectDatabase(dbName);
149
+
150
+ // Drop the database
151
+ const safeDbName = dbName.replace(/"/g, '""');
152
+ try {
153
+ await this.db.execute(sql.raw(`DROP DATABASE "${safeDbName}"`));
154
+ } catch (err) {
155
+ const msg = err instanceof Error ? err.message : String(err);
156
+ if (msg.includes("being accessed by other users")) {
157
+ throw new Error(
158
+ `Cannot delete branch "${sanitizedName}": the database has active connections. ` +
159
+ "Close other clients and try again."
160
+ );
161
+ }
162
+ throw err;
163
+ }
164
+
165
+ // Remove metadata
166
+ await this.db.execute(
167
+ sql`DELETE FROM rebase.branches WHERE name = ${sanitizedName}`
168
+ );
169
+ }
170
+
171
+ /**
172
+ * List all branches recorded in the metadata table.
173
+ * Optionally fetches database sizes from pg_database.
174
+ */
175
+ async listBranches(): Promise<BranchInfo[]> {
176
+ const result = await this.db.execute(sql.raw(`
177
+ SELECT
178
+ b.name,
179
+ b.parent_db,
180
+ b.created_at,
181
+ pg_database_size(b.db_name) as size_bytes
182
+ FROM ${BRANCHES_TABLE} b
183
+ JOIN pg_database d ON d.datname = b.db_name
184
+ ORDER BY b.created_at DESC
185
+ `));
186
+
187
+ return (result.rows as Record<string, unknown>[]).map((row) => ({
188
+ name: row.name as string,
189
+ parentDatabase: row.parent_db as string,
190
+ createdAt: new Date(row.created_at as string),
191
+ sizeBytes: row.size_bytes != null ? Number(row.size_bytes) : undefined
192
+ }));
193
+ }
194
+
195
+ /**
196
+ * Get info about a specific branch.
197
+ */
198
+ async getBranchInfo(name: string): Promise<BranchInfo | undefined> {
199
+ const sanitizedName = sanitizeBranchName(name);
200
+
201
+ const result = await this.db.execute(sql`
202
+ SELECT
203
+ b.name,
204
+ b.parent_db,
205
+ b.created_at
206
+ FROM rebase.branches b
207
+ WHERE b.name = ${sanitizedName}
208
+ `);
209
+
210
+ const rows = result.rows as Record<string, unknown>[];
211
+ if (rows.length === 0) return undefined;
212
+
213
+ const row = rows[0];
214
+
215
+ // Attempt to get size — may fail if the DB was externally dropped
216
+ let sizeBytes: number | undefined;
217
+ try {
218
+ const dbName = toBranchDbName(sanitizedName);
219
+ const sizeResult = await this.db.execute(
220
+ sql`SELECT pg_database_size(${dbName}) as size_bytes`
221
+ );
222
+ const sizeRows = sizeResult.rows as Record<string, unknown>[];
223
+ if (sizeRows.length > 0 && sizeRows[0].size_bytes != null) {
224
+ sizeBytes = Number(sizeRows[0].size_bytes);
225
+ }
226
+ } catch {
227
+ // Database might not exist anymore
228
+ }
229
+
230
+ return {
231
+ name: row.name as string,
232
+ parentDatabase: row.parent_db as string,
233
+ createdAt: new Date(row.created_at as string),
234
+ sizeBytes
235
+ };
236
+ }
237
+ }