@risingwave/wavelet-server 0.1.2 → 0.1.4

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 (2) hide show
  1. package/package.json +2 -2
  2. package/src/ddl-manager.ts +102 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@risingwave/wavelet-server",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Wavelet server - WebSocket fanout layer for RisingWave",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -13,7 +13,7 @@
13
13
  "pg": "^8.13.0",
14
14
  "ws": "^8.18.0",
15
15
  "jose": "^6.0.0",
16
- "@risingwave/wavelet": "0.1.2"
16
+ "@risingwave/wavelet": "0.1.4"
17
17
  },
18
18
  "devDependencies": {
19
19
  "@types/pg": "^8.11.0",
@@ -1,11 +1,11 @@
1
1
  import pg from 'pg'
2
- import type { WaveletConfig, StreamDef, ViewDef, SqlFragment } from '@risingwave/wavelet'
2
+ import type { WaveletConfig, StreamDef, ViewDef, SqlFragment, PostgresCdcSource } from '@risingwave/wavelet'
3
3
 
4
4
  const { Client } = pg
5
5
 
6
6
  export interface DdlAction {
7
7
  type: 'create' | 'update' | 'delete' | 'unchanged'
8
- resource: 'stream' | 'view' | 'subscription'
8
+ resource: 'stream' | 'source' | 'view' | 'subscription'
9
9
  name: string
10
10
  detail?: string
11
11
  }
@@ -75,6 +75,7 @@ export class DdlManager {
75
75
  const existingSubscriptions = await this.getExistingSubscriptions()
76
76
 
77
77
  const desiredStreams = config.streams ?? {}
78
+ const desiredSources = config.sources ?? {}
78
79
  const desiredViews = config.views ?? {}
79
80
 
80
81
  // 2. Determine which streams (tables) to create or remove
@@ -91,6 +92,22 @@ export class DdlManager {
91
92
  }
92
93
  }
93
94
 
95
+ // 3b. Sync CDC sources - create Postgres CDC tables
96
+ const existingSources = await this.getExistingSources()
97
+ for (const [sourceName, sourceDef] of Object.entries(desiredSources)) {
98
+ if (sourceDef.type === 'postgres') {
99
+ for (const tableName of sourceDef.tables) {
100
+ const cdcTableName = `${sourceName}_${tableName}`
101
+ if (existingSources.has(cdcTableName) || existingTables.has(cdcTableName)) {
102
+ actions.push({ type: 'unchanged', resource: 'source', name: cdcTableName })
103
+ } else {
104
+ await this.createCdcSource(sourceName, tableName, sourceDef)
105
+ actions.push({ type: 'create', resource: 'source', name: cdcTableName })
106
+ }
107
+ }
108
+ }
109
+ }
110
+
94
111
  // 4. Sync views - create, update, or leave unchanged
95
112
  for (const [viewName, viewDef] of Object.entries(desiredViews)) {
96
113
  const subName = `wavelet_sub_${viewName}`
@@ -207,6 +224,18 @@ export class DdlManager {
207
224
  return views
208
225
  }
209
226
 
227
+ private async getExistingSources(): Promise<Set<string>> {
228
+ try {
229
+ const result = await this.client!.query(
230
+ `SELECT name FROM rw_catalog.rw_tables WHERE schema_id = (SELECT id FROM rw_catalog.rw_schemas WHERE name = 'public') AND is_index = false`
231
+ )
232
+ return new Set(result.rows.map((r: any) => r.name))
233
+ } catch {
234
+ // Fallback: if catalog query fails, return empty set
235
+ return new Set()
236
+ }
237
+ }
238
+
210
239
  private async getExistingSubscriptions(): Promise<Set<string>> {
211
240
  const result = await this.client!.query(
212
241
  `SELECT name FROM rw_catalog.rw_subscriptions WHERE schema_id = (SELECT id FROM rw_catalog.rw_schemas WHERE name = 'public')`
@@ -304,4 +333,75 @@ export class DdlManager {
304
333
  }
305
334
  }
306
335
  }
336
+
337
+ private async createCdcSource(
338
+ sourceName: string,
339
+ tableName: string,
340
+ source: PostgresCdcSource
341
+ ): Promise<void> {
342
+ const cdcTableName = `${sourceName}_${tableName}`
343
+ const slotName = source.slotName ?? `wavelet_${sourceName}`
344
+ const pubName = source.publicationName ?? `wavelet_${sourceName}_pub`
345
+
346
+ // RisingWave CDC source syntax:
347
+ // CREATE TABLE table_name ( ... ) WITH (
348
+ // connector = 'postgres-cdc',
349
+ // hostname = '...',
350
+ // port = '...',
351
+ // username = '...',
352
+ // password = '...',
353
+ // database.name = '...',
354
+ // table.name = '...',
355
+ // slot.name = '...',
356
+ // publication.name = '...'
357
+ // )
358
+ // We parse the connection string to extract components.
359
+
360
+ const parsed = parsePostgresUrl(source.connection)
361
+
362
+ try {
363
+ await this.client!.query(`
364
+ CREATE TABLE IF NOT EXISTS ${cdcTableName} (*)
365
+ WITH (
366
+ connector = 'postgres-cdc',
367
+ hostname = '${parsed.host}',
368
+ port = '${parsed.port}',
369
+ username = '${parsed.user}',
370
+ password = '${parsed.password}',
371
+ database.name = '${parsed.database}',
372
+ schema.name = '${parsed.schema}',
373
+ table.name = '${tableName}',
374
+ slot.name = '${slotName}',
375
+ publication.name = '${pubName}'
376
+ )
377
+ `)
378
+ console.log(`[ddl-manager] Created CDC source: ${cdcTableName} (from ${parsed.host}/${parsed.database}.${tableName})`)
379
+ } catch (err: any) {
380
+ if (err.message?.includes('already exists')) {
381
+ console.log(`[ddl-manager] CDC source already exists: ${cdcTableName}`)
382
+ } else {
383
+ throw err
384
+ }
385
+ }
386
+ }
387
+ }
388
+
389
+ function parsePostgresUrl(url: string): {
390
+ host: string
391
+ port: string
392
+ user: string
393
+ password: string
394
+ database: string
395
+ schema: string
396
+ } {
397
+ // Parse postgresql://user:password@host:port/database?schema=xxx
398
+ const u = new URL(url)
399
+ return {
400
+ host: u.hostname,
401
+ port: u.port || '5432',
402
+ user: decodeURIComponent(u.username),
403
+ password: decodeURIComponent(u.password),
404
+ database: u.pathname.replace(/^\//, '') || 'postgres',
405
+ schema: u.searchParams.get('schema') ?? 'public',
406
+ }
307
407
  }