Skip to content

Commit 321996a

Browse files
committed
feat(apps/deditor): implement listUserDefinedTypes for postgres & PGLite, opt-in create vectors and vector extensions for db
1 parent c5430e8 commit 321996a

8 files changed

Lines changed: 653 additions & 41 deletions

File tree

apps/deditor/src/main/ipc/databases/local/pglite-fs.ts

Lines changed: 249 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,14 @@ import { nanoid } from '@deditor-app/shared'
66
import * as schema from '@deditor-app/shared-schemas'
77
import { postgresInformationSchemaColumns, postgresPgCatalogPgAm, postgresPgCatalogPgAttribute, postgresPgCatalogPgClass, postgresPgCatalogPgIndex, postgresPgCatalogPgNamespace, postgresPgCatalogPgType } from '@deditor-app/shared-schemas'
88
import { PGlite } from '@electric-sql/pglite'
9+
import { bloom } from '@electric-sql/pglite/contrib/bloom'
10+
import { citext } from '@electric-sql/pglite/contrib/citext'
11+
import { cube } from '@electric-sql/pglite/contrib/cube'
12+
import { earthdistance } from '@electric-sql/pglite/contrib/earthdistance'
13+
import { hstore } from '@electric-sql/pglite/contrib/hstore'
14+
import { ltree } from '@electric-sql/pglite/contrib/ltree'
15+
import { seg } from '@electric-sql/pglite/contrib/seg'
16+
import { vector } from '@electric-sql/pglite/vector'
917
import { useLogg } from '@guiiai/logg'
1018
import { and, eq, gt, ne, not, notExists, notLike, or, sql } from 'drizzle-orm'
1119
import { alias } from 'drizzle-orm/pg-core'
@@ -53,7 +61,21 @@ export function registerPGLiteDatabaseDialect(window: BrowserWindow) {
5361
try {
5462
const parsedDSN = new URL(dsn)
5563

56-
const pgliteClient = new PGlite(decodeURIComponent(String(parsedDSN.searchParams.get('dataDir'))))
64+
const pgliteClient = new PGlite(
65+
decodeURIComponent(String(parsedDSN.searchParams.get('dataDir'))),
66+
{
67+
extensions: {
68+
vector,
69+
bloom,
70+
citext,
71+
cube,
72+
earthdistance,
73+
hstore,
74+
ltree,
75+
seg,
76+
},
77+
},
78+
)
5779

5880
const pgDrizzle = drizzle(pgliteClient, { schema })
5981
const dbSessionId = nanoid()
@@ -262,6 +284,232 @@ export function registerPGLiteDatabaseDialect(window: BrowserWindow) {
262284
throw err
263285
}
264286
})
287+
288+
defineIPCHandler<PGLiteMethods>(window, 'databaseLocalPGLite', 'listUserDefinedTypes')
289+
.handle(async (_, { databaseSessionId }) => {
290+
/**
291+
* 2013 Approach to list user-defined types in PostgreSQL.
292+
* For pgvector, this method is still valid.
293+
* But I haven't touched any of the other UDT in scenarios.
294+
*
295+
* Thanks to
296+
*
297+
* postgresql - Display user-defined types and their details - Database Administrators Stack Exchange
298+
* @link{https://dba.stackexchange.com/a/35510}
299+
*
300+
* SELECT
301+
* n.nspname AS schema,
302+
* pg_catalog.format_type ( t.oid, NULL ) AS name,
303+
* t.typname AS internal_name,
304+
* CASE
305+
* WHEN t.typrelid != 0
306+
* THEN CAST ( 'tuple' AS pg_catalog.text )
307+
* WHEN t.typlen < 0
308+
* THEN CAST ( 'var' AS pg_catalog.text )
309+
* ELSE CAST ( t.typlen AS pg_catalog.text )
310+
* END AS size,
311+
* pg_catalog.array_to_string (
312+
* ARRAY(
313+
* SELECT e.enumlabel
314+
* FROM pg_catalog.pg_enum e
315+
* WHERE e.enumtypid = t.oid
316+
* ORDER BY e.oid
317+
* ), E'\n'
318+
* ) AS elements,
319+
* -- https://www.postgresql.org/docs/9.5/functions-info.html
320+
* pg_catalog.obj_description(t.oid, 'pg_type') AS description
321+
* FROM pg_catalog.pg_type t
322+
* LEFT JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace
323+
* WHERE
324+
* (
325+
* t.typrelid = 0
326+
* OR (
327+
* SELECT c.relkind = 'c'
328+
* FROM pg_catalog.pg_class c
329+
* WHERE c.oid = t.typrelid
330+
* )
331+
* ) AND
332+
* NOT EXISTS (
333+
* SELECT 1
334+
* FROM pg_catalog.pg_type el
335+
* WHERE
336+
* el.oid = t.typelem AND
337+
* el.typarray = t.oid
338+
* ) AND
339+
* -- Ignores the built-in types
340+
* n.nspname <> 'pg_catalog' AND
341+
* -- Ignores the built-in information_schema comes along types
342+
* n.nspname <> 'information_schema' AND
343+
* pg_catalog.pg_type_is_visible ( t.oid )
344+
* ORDER BY 1, 2;
345+
*/
346+
347+
/**
348+
* Thanks to
349+
*
350+
* postgresql - Display user-defined types and their details - Database Administrators Stack Exchange
351+
* @link{https://dba.stackexchange.com/a/35510}
352+
*
353+
* WITH
354+
* types AS (
355+
* SELECT
356+
* n.nspname,
357+
* pg_catalog.format_type(t.oid, NULL) AS obj_name,
358+
* CASE
359+
* WHEN t.typrelid != 0 THEN CAST ( 'tuple' AS pg_catalog.text )
360+
* WHEN t.typlen < 0 THEN CAST ( 'var' AS pg_catalog.text )
361+
* ELSE CAST ( t.typlen AS pg_catalog.text )
362+
* END AS obj_type,
363+
* coalesce(pg_catalog.obj_description(t.oid, 'pg_type'), '') AS description
364+
* FROM pg_catalog.pg_type t
365+
* JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace
366+
* WHERE (
367+
* t.typrelid = 0 OR
368+
* (
369+
* SELECT c.relkind = 'c'
370+
* FROM pg_catalog.pg_class c
371+
* WHERE c.oid = t.typrelid
372+
* )
373+
* ) AND
374+
* NOT EXISTS (
375+
* SELECT 1
376+
* FROM pg_catalog.pg_type el
377+
* WHERE
378+
* el.oid = t.typelem AND
379+
* el.typarray = t.oid
380+
* ) AND
381+
* n.nspname <> 'pg_catalog' AND
382+
* n.nspname <> 'information_schema' AND
383+
* n.nspname !~ '^pg_toast'
384+
* ),
385+
* cols AS (
386+
* SELECT
387+
* n.nspname::text AS schema_name,
388+
* pg_catalog.format_type(t.oid, NULL) AS obj_name,
389+
* a.attname::text AS column_name,
390+
* pg_catalog.format_type(a.atttypid, a.atttypmod) AS data_type,
391+
* a.attnotnull AS is_required,
392+
* a.attnum AS ordinal_position,
393+
* pg_catalog.col_description(a.attrelid, a.attnum) AS description
394+
* FROM pg_catalog.pg_attribute a
395+
* JOIN pg_catalog.pg_type t ON a.attrelid = t.typrelid
396+
* JOIN pg_catalog.pg_namespace n ON ( n.oid = t.typnamespace )
397+
* JOIN types ON ( types.nspname = n.nspname AND types.obj_name = pg_catalog.format_type(t.oid, NULL) )
398+
* WHERE
399+
* a.attnum > 0 AND
400+
* NOT a.attisdropped
401+
* )
402+
*
403+
* SELECT
404+
* cols.schema_name,
405+
* cols.obj_name,
406+
* cols.column_name,
407+
* cols.data_type,
408+
* cols.ordinal_position,
409+
* cols.is_required,
410+
* coalesce(cols.description, '') AS description
411+
* FROM cols
412+
* ORDER BY cols.schema_name, cols.obj_name, cols.ordinal_position;
413+
*/
414+
if (!databaseSessions.has(databaseSessionId)) {
415+
throw new Error('Database session ID not found in session map, please connect to the database first.')
416+
}
417+
418+
try {
419+
const dbSession = databaseSessions.get(databaseSessionId)!
420+
const typesSubQuery = dbSession.drizzle
421+
.$with('types')
422+
.as(
423+
dbSession.drizzle.select({
424+
nspname: postgresPgCatalogPgNamespace.nspname,
425+
objName: sql<string>`${sql.identifier('pg_catalog')}.${sql.identifier('format_type')}(${postgresPgCatalogPgType.oid}, NULL)`.as('obj_name'),
426+
objType: sql<string>`
427+
CASE
428+
WHEN ${postgresPgCatalogPgType.typrelid} != 0 THEN CAST('tuple' AS pg_catalog.text)
429+
WHEN ${postgresPgCatalogPgType.typlen} < 0 THEN CAST('var' AS pg_catalog.text)
430+
ELSE CAST(${postgresPgCatalogPgType.typlen} AS pg_catalog.text)
431+
END`.as('obj_type'),
432+
description: sql<string>`${sql.identifier('pg_catalog')}.${sql.identifier('obj_description')}(${postgresPgCatalogPgType.oid}, 'pg_type')`.as('description'),
433+
})
434+
.from(postgresPgCatalogPgType)
435+
.leftJoin(postgresPgCatalogPgNamespace, eq(postgresPgCatalogPgType.typnamespace, postgresPgCatalogPgNamespace.oid))
436+
.where(
437+
and(
438+
or(
439+
eq(postgresPgCatalogPgType.typrelid, 0),
440+
dbSession.drizzle
441+
.select({ c: eq(postgresPgCatalogPgClass.relkind, 'c') })
442+
.from(postgresPgCatalogPgClass)
443+
.where(eq(postgresPgCatalogPgClass.oid, postgresPgCatalogPgType.typrelid)),
444+
),
445+
notExists(
446+
dbSession.drizzle
447+
.select({
448+
1: sql`1`,
449+
})
450+
.from(postgresPgCatalogPgType)
451+
.where(
452+
and(
453+
eq(postgresPgCatalogPgType.oid, postgresPgCatalogPgType.typelem),
454+
eq(postgresPgCatalogPgType.typarray, postgresPgCatalogPgType.oid),
455+
),
456+
),
457+
),
458+
ne(postgresPgCatalogPgNamespace.nspname, 'pg_catalog'),
459+
ne(postgresPgCatalogPgNamespace.nspname, 'information_schema'),
460+
notLike(postgresPgCatalogPgNamespace.nspname, 'pg_toast%'),
461+
),
462+
),
463+
)
464+
465+
const colsSubQuery = dbSession.drizzle
466+
.$with('cols')
467+
.as(
468+
dbSession.drizzle.select({
469+
schemaName: postgresPgCatalogPgNamespace.nspname,
470+
objName: sql<string>`${sql.identifier('pg_catalog')}.${sql.identifier('format_type')}(${postgresPgCatalogPgType.oid}, NULL)`.as('obj_name'),
471+
columnName: postgresPgCatalogPgAttribute.attname,
472+
dataType: sql<string>`${sql.identifier('pg_catalog')}.${sql.identifier('format_type')}(${postgresPgCatalogPgAttribute.atttypid}, ${postgresPgCatalogPgAttribute.atttypmod})`.as('data_type'),
473+
isRequired: postgresPgCatalogPgAttribute.attnotnull,
474+
ordinalPosition: postgresPgCatalogPgAttribute.attnum,
475+
description: sql<string>`${sql.identifier('pg_catalog')}.${sql.identifier('col_description')}(${postgresPgCatalogPgAttribute.attrelid}, ${postgresPgCatalogPgAttribute.attnum})`.as('description'),
476+
})
477+
.from(postgresPgCatalogPgAttribute)
478+
.leftJoin(postgresPgCatalogPgType, eq(postgresPgCatalogPgAttribute.atttypid, postgresPgCatalogPgType.oid))
479+
.leftJoin(postgresPgCatalogPgNamespace, eq(postgresPgCatalogPgType.typnamespace, postgresPgCatalogPgNamespace.oid))
480+
.leftJoin(sql`types`, and(
481+
eq(typesSubQuery.nspname, postgresPgCatalogPgNamespace.nspname),
482+
eq(typesSubQuery.objName, sql<string>`${sql.identifier('pg_catalog')}.${sql.identifier('format_type')}(${postgresPgCatalogPgType.oid}, NULL)`),
483+
))
484+
.where(
485+
and(
486+
gt(postgresPgCatalogPgAttribute.attnum, 0),
487+
eq(postgresPgCatalogPgAttribute.attisdropped, false),
488+
),
489+
),
490+
)
491+
492+
const results = await dbSession.drizzle
493+
.with(typesSubQuery, colsSubQuery)
494+
.select({
495+
schemaName: colsSubQuery.schemaName,
496+
objName: colsSubQuery.objName,
497+
columnName: colsSubQuery.columnName,
498+
dataType: colsSubQuery.dataType,
499+
ordinalPosition: colsSubQuery.ordinalPosition,
500+
isRequired: colsSubQuery.isRequired,
501+
description: sql<string>`coalesce(${colsSubQuery.description}, '')`.as('description'),
502+
})
503+
.from(colsSubQuery)
504+
.orderBy(colsSubQuery.schemaName, colsSubQuery.objName, colsSubQuery.ordinalPosition)
505+
506+
return {
507+
databaseSessionId,
508+
results,
509+
}
510+
}
511+
catch (err) {
512+
log.withError(err).withFields({ databaseSessionId }).error('failed to query local PGLite database to list user-defined types')
265513
throw err
266514
}
267515
})

0 commit comments

Comments
 (0)