@o/database 0.16.18 → 0.16.19-canary.1790191946359

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 (121) hide show
  1. package/dist/takeout/better-auth.d.ts +19 -0
  2. package/dist/takeout/better-auth.d.ts.map +1 -0
  3. package/dist/takeout/better-auth.js +59 -0
  4. package/dist/takeout/better-auth.js.map +1 -0
  5. package/dist/takeout/build.d.ts +2 -0
  6. package/dist/takeout/build.d.ts.map +1 -0
  7. package/dist/takeout/build.js +3 -0
  8. package/dist/takeout/build.js.map +1 -0
  9. package/dist/takeout/chunkedQuery.d.ts +17 -0
  10. package/dist/takeout/chunkedQuery.d.ts.map +1 -0
  11. package/dist/takeout/chunkedQuery.js +55 -0
  12. package/dist/takeout/chunkedQuery.js.map +1 -0
  13. package/dist/takeout/cli.d.ts +3 -0
  14. package/dist/takeout/cli.d.ts.map +1 -0
  15. package/dist/takeout/cli.js +114 -0
  16. package/dist/takeout/cli.js.map +1 -0
  17. package/dist/takeout/createDatabase.d.ts +49 -0
  18. package/dist/takeout/createDatabase.d.ts.map +1 -0
  19. package/dist/takeout/createDatabase.js +100 -0
  20. package/dist/takeout/createDatabase.js.map +1 -0
  21. package/dist/takeout/createDb.d.ts +3 -0
  22. package/dist/takeout/createDb.d.ts.map +1 -0
  23. package/dist/takeout/createDb.js +11 -0
  24. package/dist/takeout/createDb.js.map +1 -0
  25. package/dist/takeout/createDrizzle.d.ts +38 -0
  26. package/dist/takeout/createDrizzle.d.ts.map +1 -0
  27. package/dist/takeout/createDrizzle.js +60 -0
  28. package/dist/takeout/createDrizzle.js.map +1 -0
  29. package/dist/takeout/createPool.d.ts +11 -0
  30. package/dist/takeout/createPool.d.ts.map +1 -0
  31. package/dist/takeout/createPool.js +36 -0
  32. package/dist/takeout/createPool.js.map +1 -0
  33. package/dist/takeout/createServerHelpers.d.ts +9 -0
  34. package/dist/takeout/createServerHelpers.d.ts.map +1 -0
  35. package/dist/takeout/createServerHelpers.js +11 -0
  36. package/dist/takeout/createServerHelpers.js.map +1 -0
  37. package/dist/takeout/getDBClient.d.ts +11 -0
  38. package/dist/takeout/getDBClient.d.ts.map +1 -0
  39. package/dist/takeout/getDBClient.js +169 -0
  40. package/dist/takeout/getDBClient.js.map +1 -0
  41. package/dist/takeout/index.d.ts +13 -0
  42. package/dist/takeout/index.d.ts.map +1 -0
  43. package/dist/takeout/index.js +13 -0
  44. package/dist/takeout/index.js.map +1 -0
  45. package/dist/takeout/migrate.d.ts +33 -0
  46. package/dist/takeout/migrate.d.ts.map +1 -0
  47. package/dist/takeout/migrate.js +214 -0
  48. package/dist/takeout/migrate.js.map +1 -0
  49. package/dist/takeout/scripts/build-migrations.d.ts +8 -0
  50. package/dist/takeout/scripts/build-migrations.d.ts.map +1 -0
  51. package/dist/takeout/scripts/build-migrations.js +51 -0
  52. package/dist/takeout/scripts/build-migrations.js.map +1 -0
  53. package/dist/takeout/scripts/drizzle-migrations-sync.d.ts +17 -0
  54. package/dist/takeout/scripts/drizzle-migrations-sync.d.ts.map +1 -0
  55. package/dist/takeout/scripts/drizzle-migrations-sync.js +69 -0
  56. package/dist/takeout/scripts/drizzle-migrations-sync.js.map +1 -0
  57. package/dist/takeout/scripts/dump-database.d.ts +22 -0
  58. package/dist/takeout/scripts/dump-database.d.ts.map +1 -0
  59. package/dist/takeout/scripts/dump-database.js +64 -0
  60. package/dist/takeout/scripts/dump-database.js.map +1 -0
  61. package/dist/takeout/scripts/migration-add.d.ts +6 -0
  62. package/dist/takeout/scripts/migration-add.d.ts.map +1 -0
  63. package/dist/takeout/scripts/migration-add.js +39 -0
  64. package/dist/takeout/scripts/migration-add.js.map +1 -0
  65. package/dist/takeout/scripts/pg_dump.d.ts +11 -0
  66. package/dist/takeout/scripts/pg_dump.d.ts.map +1 -0
  67. package/dist/takeout/scripts/pg_dump.js +37 -0
  68. package/dist/takeout/scripts/pg_dump.js.map +1 -0
  69. package/dist/takeout/scripts/prune-backups.d.ts +8 -0
  70. package/dist/takeout/scripts/prune-backups.d.ts.map +1 -0
  71. package/dist/takeout/scripts/prune-backups.js +166 -0
  72. package/dist/takeout/scripts/prune-backups.js.map +1 -0
  73. package/dist/takeout/scripts/psql.d.ts +11 -0
  74. package/dist/takeout/scripts/psql.d.ts.map +1 -0
  75. package/dist/takeout/scripts/psql.js +41 -0
  76. package/dist/takeout/scripts/psql.js.map +1 -0
  77. package/dist/takeout/seed.d.ts +10 -0
  78. package/dist/takeout/seed.d.ts.map +1 -0
  79. package/dist/takeout/seed.js +22 -0
  80. package/dist/takeout/seed.js.map +1 -0
  81. package/dist/takeout/sql.d.ts +9 -0
  82. package/dist/takeout/sql.d.ts.map +1 -0
  83. package/dist/takeout/sql.js +24 -0
  84. package/dist/takeout/sql.js.map +1 -0
  85. package/dist/takeout/sqlite.d.ts +66 -0
  86. package/dist/takeout/sqlite.d.ts.map +1 -0
  87. package/dist/takeout/sqlite.js +110 -0
  88. package/dist/takeout/sqlite.js.map +1 -0
  89. package/dist/takeout/waitForDatabase.d.ts +2 -0
  90. package/dist/takeout/waitForDatabase.d.ts.map +1 -0
  91. package/dist/takeout/waitForDatabase.js +26 -0
  92. package/dist/takeout/waitForDatabase.js.map +1 -0
  93. package/dist/takeout/zero.d.ts +44 -0
  94. package/dist/takeout/zero.d.ts.map +1 -0
  95. package/dist/takeout/zero.js +177 -0
  96. package/dist/takeout/zero.js.map +1 -0
  97. package/package.json +54 -4
  98. package/src/takeout/better-auth.ts +110 -0
  99. package/src/takeout/build.ts +2 -0
  100. package/src/takeout/chunkedQuery.ts +91 -0
  101. package/src/takeout/cli.ts +126 -0
  102. package/src/takeout/createDatabase.ts +164 -0
  103. package/src/takeout/createDb.ts +15 -0
  104. package/src/takeout/createDrizzle.ts +127 -0
  105. package/src/takeout/createPool.ts +58 -0
  106. package/src/takeout/createServerHelpers.ts +21 -0
  107. package/src/takeout/getDBClient.ts +254 -0
  108. package/src/takeout/index.ts +38 -0
  109. package/src/takeout/migrate.ts +272 -0
  110. package/src/takeout/scripts/build-migrations.ts +66 -0
  111. package/src/takeout/scripts/drizzle-migrations-sync.ts +81 -0
  112. package/src/takeout/scripts/dump-database.ts +103 -0
  113. package/src/takeout/scripts/migration-add.ts +54 -0
  114. package/src/takeout/scripts/pg_dump.ts +46 -0
  115. package/src/takeout/scripts/prune-backups.ts +211 -0
  116. package/src/takeout/scripts/psql.ts +51 -0
  117. package/src/takeout/seed.ts +45 -0
  118. package/src/takeout/sql.ts +39 -0
  119. package/src/takeout/sqlite.ts +237 -0
  120. package/src/takeout/waitForDatabase.ts +27 -0
  121. package/src/takeout/zero.ts +281 -0
@@ -0,0 +1,237 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks'
2
+
3
+ import { drizzle, type SqliteRemoteDatabase } from 'drizzle-orm/sqlite-proxy'
4
+
5
+ import type { Database, SQLQueryBindings } from 'bun:sqlite'
6
+ import type { AnyRelations, EmptyRelations } from 'drizzle-orm'
7
+ import type { SQLiteTable } from 'drizzle-orm/sqlite-core'
8
+
9
+ export type SqlStatementMetadata = {
10
+ table: string
11
+ publicTable: string
12
+ kind: 'delete' | 'insert' | 'update' | 'upsert'
13
+ }
14
+
15
+ export type SQLiteQueryFormat = {
16
+ relationships: Record<string, SQLiteQueryFormat>
17
+ singular: boolean
18
+ }
19
+
20
+ export type SQLiteExecResult = {
21
+ changes: number
22
+ }
23
+
24
+ export type SQLiteStatement = {
25
+ sql: string
26
+ params?: readonly unknown[]
27
+ metadata?: SqlStatementMetadata
28
+ }
29
+
30
+ export type SQLiteTransactionExecutor = {
31
+ exec(
32
+ sql: string,
33
+ params?: readonly unknown[],
34
+ metadata?: SqlStatementMetadata
35
+ ): SQLiteExecResult | Promise<SQLiteExecResult>
36
+ /**
37
+ * every statement in one call to the storage owner, in order, as one
38
+ * atomic step. a remote owner holds its writer across each round trip, so a
39
+ * list sent one statement at a time holds it for the whole exchange.
40
+ */
41
+ execMany(
42
+ statements: readonly SQLiteStatement[]
43
+ ): SQLiteExecResult[] | Promise<SQLiteExecResult[]>
44
+ query<Row extends Record<string, unknown> = Record<string, unknown>>(
45
+ sql: string,
46
+ params?: readonly unknown[]
47
+ ): Row[] | Promise<Row[]>
48
+ queryAst<Result = unknown>(
49
+ ast: unknown,
50
+ format: SQLiteQueryFormat,
51
+ queryName?: string
52
+ ): Result | Promise<Result>
53
+ }
54
+
55
+ export type SQLiteTransactionProvider = <Value>(
56
+ work: (executor: SQLiteTransactionExecutor) => Value | Promise<Value>
57
+ ) => Promise<Value>
58
+
59
+ export type SQLiteSchema = Record<string, SQLiteTable>
60
+
61
+ export type CreateSQLiteDatabaseOptions<
62
+ TSchema extends SQLiteSchema,
63
+ TRelations extends AnyRelations = EmptyRelations,
64
+ > = {
65
+ schema: TSchema
66
+ relations?: TRelations
67
+ transactionProvider: SQLiteTransactionProvider
68
+ }
69
+
70
+ export type SQLiteDatabaseTransaction<
71
+ TSchema extends SQLiteSchema,
72
+ TRelations extends AnyRelations,
73
+ > = {
74
+ executor: SQLiteTransactionExecutor
75
+ drizzle: SqliteRemoteDatabase<TSchema, TRelations>
76
+ }
77
+
78
+ export type SQLiteDatabase<
79
+ TSchema extends SQLiteSchema,
80
+ TRelations extends AnyRelations,
81
+ > = {
82
+ transaction<Value>(
83
+ work: (
84
+ transaction: SQLiteDatabaseTransaction<TSchema, TRelations>
85
+ ) => Value | Promise<Value>
86
+ ): Promise<Value>
87
+ }
88
+
89
+ /**
90
+ * Creates Drizzle only after the storage owner has started its transaction.
91
+ * The sqlite-proxy driver is used solely to turn its query callbacks into
92
+ * typed Drizzle operations; it never owns BEGIN or COMMIT.
93
+ */
94
+ export function createSQLiteDatabase<
95
+ TSchema extends SQLiteSchema,
96
+ TRelations extends AnyRelations = EmptyRelations,
97
+ >(
98
+ options: CreateSQLiteDatabaseOptions<TSchema, TRelations>
99
+ ): SQLiteDatabase<TSchema, TRelations> {
100
+ return {
101
+ transaction(work) {
102
+ return options.transactionProvider(async (executor) =>
103
+ work({
104
+ executor,
105
+ drizzle: createSQLiteDrizzle(executor, options),
106
+ })
107
+ )
108
+ },
109
+ }
110
+ }
111
+
112
+ export function createSQLiteDrizzle<
113
+ TSchema extends SQLiteSchema,
114
+ TRelations extends AnyRelations = EmptyRelations,
115
+ >(
116
+ executor: SQLiteTransactionExecutor,
117
+ options: Pick<CreateSQLiteDatabaseOptions<TSchema, TRelations>, 'relations' | 'schema'>
118
+ ): SqliteRemoteDatabase<TSchema, TRelations> {
119
+ return drizzle<TSchema, TRelations>(
120
+ async (sql, params, method) => {
121
+ if (method === 'run') {
122
+ const result = await executor.exec(sql, params)
123
+ return { changes: result.changes, rows: [] }
124
+ }
125
+
126
+ const rows = await executor.query(sql, params)
127
+ if (method === 'get') {
128
+ const row = rows[0]
129
+ return remoteRows(row ? rowValues(row) : undefined)
130
+ }
131
+ return remoteRows(rows.map(rowValues))
132
+ },
133
+ {
134
+ schema: options.schema,
135
+ relations: options.relations,
136
+ }
137
+ )
138
+ }
139
+
140
+ export type CreateBunSQLiteExecutorOptions = {
141
+ database: Database
142
+ queryAst<Result = unknown>(
143
+ ast: unknown,
144
+ format: SQLiteQueryFormat,
145
+ queryName?: string
146
+ ): Result | Promise<Result>
147
+ }
148
+
149
+ export function createBunSQLiteExecutor({
150
+ database,
151
+ queryAst,
152
+ }: CreateBunSQLiteExecutorOptions): SQLiteTransactionExecutor {
153
+ return {
154
+ exec(sql, params = []) {
155
+ const result = database.prepare(sql).run(...bunBindings(params))
156
+ return { changes: result.changes }
157
+ },
158
+ execMany(statements) {
159
+ return statements.map(({ sql, params = [] }) => ({
160
+ changes: database.prepare(sql).run(...bunBindings(params)).changes,
161
+ }))
162
+ },
163
+ query<Row extends Record<string, unknown>>(
164
+ sql: string,
165
+ params: readonly unknown[] = []
166
+ ): Row[] {
167
+ return database.prepare<Row, SQLQueryBindings[]>(sql).all(...bunBindings(params))
168
+ },
169
+ queryAst,
170
+ }
171
+ }
172
+
173
+ export type CreateBunSQLiteTransactionProviderOptions = CreateBunSQLiteExecutorOptions
174
+
175
+ /**
176
+ * Bun's synchronous driver needs its own native transaction boundary. Remote
177
+ * and Durable Object hosts pass their protected callback directly to
178
+ * createSQLiteDatabase instead.
179
+ */
180
+ export function createBunSQLiteTransactionProvider({
181
+ database,
182
+ queryAst,
183
+ }: CreateBunSQLiteTransactionProviderOptions): SQLiteTransactionProvider {
184
+ const executor = createBunSQLiteExecutor({ database, queryAst })
185
+ const transactionContext = new AsyncLocalStorage<boolean>()
186
+ let tail = Promise.resolve()
187
+
188
+ return async (work) => {
189
+ if (transactionContext.getStore()) {
190
+ throw new Error('nested SQLite transactions are not supported')
191
+ }
192
+ // keep the native connection's transaction state exclusive across awaits.
193
+ const previous = tail
194
+ let release!: () => void
195
+ tail = new Promise<void>((resolve) => {
196
+ release = resolve
197
+ })
198
+
199
+ await previous
200
+ try {
201
+ return await transactionContext.run(true, async () => {
202
+ let began = false
203
+ try {
204
+ database.exec('BEGIN')
205
+ began = true
206
+ const value = await work(executor)
207
+ database.exec('COMMIT')
208
+ began = false
209
+ return value
210
+ } catch (error) {
211
+ if (began) database.exec('ROLLBACK')
212
+ throw error
213
+ }
214
+ })
215
+ } finally {
216
+ release()
217
+ }
218
+ }
219
+ }
220
+
221
+ function rowValues(row: Record<string, unknown>): unknown[] {
222
+ return Object.values(row)
223
+ }
224
+
225
+ function remoteRows(rows: unknown[] | undefined): { rows: any[] } {
226
+ // Drizzle's sqlite-proxy declaration requires an array even though its get()
227
+ // implementation recognizes an absent row. Keep that transport mismatch at
228
+ // this one adapter boundary rather than turning a missing row into one full
229
+ // of undefined column values.
230
+ return { rows: rows as any[] }
231
+ }
232
+
233
+ function bunBindings(params: readonly unknown[]): SQLQueryBindings[] {
234
+ // on-zero accepts unknown bindings so every native provider can expose one
235
+ // structural contract. Bun validates these at its typed boundary.
236
+ return params as SQLQueryBindings[]
237
+ }
@@ -0,0 +1,27 @@
1
+ import pg from 'pg'
2
+
3
+ export async function waitForDatabase(connectionString: string, maxRetries = 30) {
4
+ for (let i = 0; i < maxRetries; i++) {
5
+ try {
6
+ const client = new pg.Client({
7
+ connectionString,
8
+ connectionTimeoutMillis: 5_000,
9
+ ssl: connectionString.includes('sslmode=require')
10
+ ? { rejectUnauthorized: false }
11
+ : undefined,
12
+ })
13
+ await client.connect()
14
+ await client.query('SELECT 1')
15
+ await client.end()
16
+ console.info('[database] connection successful')
17
+ return
18
+ } catch {
19
+ const delay = Math.min(1000 * 1.5 ** i, 10000)
20
+ console.info(
21
+ `[database] waiting... attempt ${i + 1}/${maxRetries} (retry in ${delay}ms)`
22
+ )
23
+ await new Promise((resolve) => setTimeout(resolve, delay))
24
+ }
25
+ }
26
+ throw new Error('database connection timeout after ' + maxRetries + ' attempts')
27
+ }
@@ -0,0 +1,281 @@
1
+ import { basename } from 'node:path'
2
+
3
+ import { getTableName } from 'drizzle-orm'
4
+ import { PgTable } from 'drizzle-orm/pg-core'
5
+
6
+ import { getDBClient } from './getDBClient.js'
7
+
8
+ import type { PoolClient } from 'pg'
9
+
10
+ // --- zero publication management ---
11
+
12
+ export type ZeroPublicationOptions = {
13
+ connectionString: string
14
+ /** zero publication name, e.g. 'zero_takeout' or 'zero_chat' */
15
+ publicationName: string
16
+ /** table names excluded from publication (private/auth tables) */
17
+ privateTableNames: string[]
18
+ }
19
+
20
+ export async function ensureZeroPublication(options: ZeroPublicationOptions) {
21
+ const { connectionString, publicationName, privateTableNames } = options
22
+ const client = await getDBClient({ connectionString })
23
+
24
+ try {
25
+ const { rows: wanted } = await client.query(
26
+ `SELECT tablename FROM pg_tables
27
+ WHERE schemaname = 'public'
28
+ AND tablename != ALL($1)
29
+ AND tablename NOT LIKE '_zero_%'
30
+ AND tablename != 'migrations'`,
31
+ [privateTableNames]
32
+ )
33
+ const wantedSet = new Set(wanted.map((r: any) => r.tablename))
34
+
35
+ if (!wantedSet.size) {
36
+ console.info(`[zero] no public tables found for ${publicationName} publication`)
37
+ return
38
+ }
39
+
40
+ const { rows: pub } = await client.query(
41
+ `SELECT 1 FROM pg_publication WHERE pubname = $1`,
42
+ [publicationName]
43
+ )
44
+
45
+ if (!pub.length) {
46
+ try {
47
+ const tableList = [...wantedSet].map((t) => `"${t}"`).join(', ')
48
+ await client.query(`CREATE PUBLICATION ${publicationName} FOR TABLE ${tableList}`)
49
+ console.info(
50
+ `[zero] created publication ${publicationName} with ${wantedSet.size} tables`
51
+ )
52
+ } catch (e: any) {
53
+ if (e.code === '42710') {
54
+ console.info(
55
+ `[zero] ${publicationName} publication was created concurrently, will sync`
56
+ )
57
+ } else {
58
+ console.warn(
59
+ `[zero] could not create ${publicationName} publication: ${e.message}`
60
+ )
61
+ return
62
+ }
63
+ }
64
+ }
65
+
66
+ const { rows: current } = await client.query(
67
+ `SELECT tablename FROM pg_publication_tables
68
+ WHERE pubname = $1 AND schemaname = 'public'`,
69
+ [publicationName]
70
+ )
71
+ const currentSet = new Set(current.map((r: any) => r.tablename))
72
+
73
+ const toRemove = [...currentSet].filter((t) => !wantedSet.has(t))
74
+ if (toRemove.length) {
75
+ const dropList = toRemove.map((t) => `"${t}"`).join(', ')
76
+ await client.query(`ALTER PUBLICATION ${publicationName} DROP TABLE ${dropList}`)
77
+ console.info(`[zero] removed from ${publicationName}: ${dropList}`)
78
+ }
79
+
80
+ const toAdd = [...wantedSet].filter((t) => !currentSet.has(t))
81
+ const added: string[] = []
82
+ for (const table of toAdd) {
83
+ try {
84
+ await client.query(`ALTER PUBLICATION ${publicationName} ADD TABLE "${table}"`)
85
+ added.push(table)
86
+ } catch (e: any) {
87
+ if (e.code !== '42710') throw e
88
+ }
89
+ }
90
+
91
+ if (added.length) {
92
+ console.info(
93
+ `[zero] added to ${publicationName}: ${added.map((t) => `"${t}"`).join(', ')}`
94
+ )
95
+ }
96
+
97
+ if (!toRemove.length && !added.length) {
98
+ console.info(
99
+ `[zero] ${publicationName} publication is up to date (${currentSet.size} tables)`
100
+ )
101
+ }
102
+ } finally {
103
+ client.release()
104
+ }
105
+ }
106
+
107
+ // --- zero database creation ---
108
+
109
+ export type ZeroDatabaseOptions = {
110
+ connectionString: string
111
+ cvrDb?: string
112
+ changeDb?: string
113
+ }
114
+
115
+ export async function ensureZeroDatabases(options: ZeroDatabaseOptions) {
116
+ const { connectionString, cvrDb, changeDb } = options
117
+ if (!cvrDb && !changeDb) return
118
+
119
+ const client = await getDBClient({ connectionString })
120
+
121
+ try {
122
+ const dbNames = [basename(cvrDb || ''), basename(changeDb || '')].filter(Boolean)
123
+
124
+ for (const name of dbNames) {
125
+ const { rows } = await client.query(
126
+ `SELECT 1 FROM pg_database WHERE datname = '${name}'`
127
+ )
128
+ if (!rows.length) {
129
+ await client.query(`CREATE DATABASE ${name};`)
130
+ console.info(`[zero] created database ${name}`)
131
+ }
132
+ }
133
+ } finally {
134
+ client.release()
135
+ }
136
+ }
137
+
138
+ // --- zero triggers ---
139
+
140
+ export type TriggerDefinition = {
141
+ name: string
142
+ sql: string
143
+ }
144
+
145
+ export function defineTrigger(name: string, sql: string): TriggerDefinition {
146
+ return { name, sql }
147
+ }
148
+
149
+ /** load trigger definitions from a glob result (import.meta.glob or loadTriggersFromDir) */
150
+ export async function loadTriggers(
151
+ glob: Record<string, () => Promise<unknown>>
152
+ ): Promise<TriggerDefinition[]> {
153
+ const triggers: TriggerDefinition[] = []
154
+ for (const [path, load] of Object.entries(glob).sort(([a], [b]) =>
155
+ a.localeCompare(b)
156
+ )) {
157
+ const mod = (await load()) as {
158
+ default?: TriggerDefinition
159
+ name?: string
160
+ sql?: string
161
+ }
162
+ if (mod.default) {
163
+ triggers.push(mod.default)
164
+ } else if (mod.sql) {
165
+ triggers.push({ name: mod.name || basename(path, '.ts'), sql: mod.sql })
166
+ }
167
+ }
168
+ return triggers
169
+ }
170
+
171
+ export async function ensureZeroTriggers(
172
+ client: PoolClient,
173
+ triggers: TriggerDefinition[]
174
+ ) {
175
+ for (const trigger of triggers) {
176
+ try {
177
+ await client.query(trigger.sql)
178
+ } catch (err) {
179
+ console.error(`[zero] failed to apply trigger ${trigger.name}:`, err)
180
+ throw err
181
+ }
182
+ }
183
+ console.info(`[zero] applied ${triggers.length} trigger definitions`)
184
+ }
185
+
186
+ // --- zero seeds ---
187
+
188
+ export type SeedDefinition = {
189
+ name: string
190
+ sql: string
191
+ }
192
+
193
+ export function defineSeed(name: string, sql: string): SeedDefinition {
194
+ return { name, sql }
195
+ }
196
+
197
+ export async function loadSeeds(
198
+ glob: Record<string, () => Promise<unknown>>
199
+ ): Promise<SeedDefinition[]> {
200
+ const seeds: SeedDefinition[] = []
201
+ for (const [path, load] of Object.entries(glob).sort(([a], [b]) =>
202
+ a.localeCompare(b)
203
+ )) {
204
+ const mod = (await load()) as {
205
+ default?: SeedDefinition
206
+ name?: string
207
+ sql?: string
208
+ }
209
+ if (mod.default) {
210
+ seeds.push(mod.default)
211
+ } else if (mod.sql) {
212
+ seeds.push({ name: mod.name || basename(path, '.ts'), sql: mod.sql })
213
+ }
214
+ }
215
+ return seeds
216
+ }
217
+
218
+ export async function ensureZeroSeeds(client: PoolClient, seeds: SeedDefinition[]) {
219
+ for (const seed of seeds) {
220
+ try {
221
+ await client.query(seed.sql)
222
+ } catch (err) {
223
+ console.error(`[zero] failed to apply seed ${seed.name}:`, err)
224
+ throw err
225
+ }
226
+ }
227
+ console.info(`[zero] applied ${seeds.length} seed definitions`)
228
+ }
229
+
230
+ // --- init sql for pglite/browser ---
231
+ // todo: drizzleSchemaToSQL(schema) → string for browser pglite init (currently ddl is passed as string)
232
+
233
+ export type InitSqlOptions = {
234
+ /** DDL statements (CREATE TABLE, etc.) */
235
+ ddl?: string
236
+ triggers?: TriggerDefinition[]
237
+ seeds?: SeedDefinition[]
238
+ }
239
+
240
+ export function buildInitSql(options: InitSqlOptions): string {
241
+ const parts: string[] = []
242
+
243
+ if (options.ddl) {
244
+ parts.push(options.ddl)
245
+ }
246
+
247
+ if (options.triggers?.length) {
248
+ parts.push(...options.triggers.map((t) => `-- trigger: ${t.name}\n${t.sql}`))
249
+ }
250
+
251
+ if (options.seeds?.length) {
252
+ parts.push(...options.seeds.map((s) => `-- seed: ${s.name}\n${s.sql}`))
253
+ }
254
+
255
+ return parts.join('\n\n')
256
+ }
257
+
258
+ // --- schema helpers ---
259
+
260
+ const PRIVATE = Symbol.for('take-out/database/private')
261
+
262
+ /** wrap pgTable to mark tables as private (excluded from zero replication) */
263
+ export function privateTable<T extends (...args: any[]) => any>(createTable: T): T {
264
+ return ((...args: any[]) => {
265
+ const table = createTable(...args)
266
+ ;(table as any)[PRIVATE] = true
267
+ return table
268
+ }) as unknown as T
269
+ }
270
+
271
+ /** check if a table is marked as private */
272
+ export function isPrivateTable(table: unknown): boolean {
273
+ return !!(table && typeof table === 'object' && PRIVATE in table)
274
+ }
275
+
276
+ /** derive public table names from a schema (all tables not marked private) */
277
+ export function derivePublicTables(schema: Record<string, unknown>): string[] {
278
+ return Object.values(schema)
279
+ .filter((v) => v instanceof PgTable && !isPrivateTable(v))
280
+ .map((t) => getTableName(t as PgTable))
281
+ }