Skip to content

Added conventional resolution of the connector using connection string #112

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -125,4 +125,6 @@ lib
*/.output
e2e/esmCompatibility/.output
src/e2e/esmCompatibility/.output
**/0x
**/0x

**/*.db
11 changes: 8 additions & 3 deletions src/packages/dumbo/src/benchmarks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,16 @@ import 'dotenv/config';
import Benchmark from 'benchmark';
import pg from 'pg';
import { rawSql, single } from '..';
import { defaultPostgreSQLConenctionString, dumbo } from '../pg';
import {
defaultPostgreSQLConenctionString,
dumbo,
PostgreSQLConnectionString,
} from '../pg';

const connectionString =
const connectionString = PostgreSQLConnectionString(
process.env.BENCHMARK_POSTGRESQL_CONNECTION_STRING ??
defaultPostgreSQLConenctionString;
defaultPostgreSQLConenctionString,
);

const pooled = process.env.BENCHMARK_CONNECTION_POOLED === 'true';

Expand Down
53 changes: 52 additions & 1 deletion src/packages/dumbo/src/core/connections/connection.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { ConnectorType } from '../connectors';
import {
createDeferredExecutor,
sqlExecutor,
type DbSQLExecutor,
type WithSQLExecutor,
Expand All @@ -14,7 +15,7 @@ export interface Connection<
Connector extends ConnectorType = ConnectorType,
DbClient = unknown,
> extends WithSQLExecutor,
DatabaseTransactionFactory<Connector> {
DatabaseTransactionFactory<Connector, DbClient> {
connector: Connector;
open: () => Promise<DbClient>;
close: () => Promise<void>;
Expand Down Expand Up @@ -85,3 +86,53 @@ export const createConnection = <

return typedConnection;
};

export const createDeferredConnection = <Connector extends ConnectorType>(
connector: Connector,
importConnection: () => Promise<Connection<Connector>>,
): Connection<Connector> => {
const getConnection = importConnection();

const execute = createDeferredExecutor(async () => {
const conn = await getConnection;
return conn.execute;
});

const connection: Connection<Connector> = {
connector,
execute,

open: async (): Promise<unknown> => {
const conn = await getConnection;
return conn.open();
},

close: async (): Promise<void> => {
if (getConnection) {
const conn = await getConnection;
await conn.close();
}
},

transaction: () => {
const transaction = getConnection.then((c) => c.transaction());

return {
connector,
connection,
execute: createDeferredExecutor(
async () => (await transaction).execute,
),
begin: async () => (await transaction).begin(),
commit: async () => (await transaction).commit(),
rollback: async () => (await transaction).rollback(),
};
},
withTransaction: async (handle) => {
const connection = await getConnection;
return connection.withTransaction(handle);
},
};

return connection;
};
39 changes: 39 additions & 0 deletions src/packages/dumbo/src/core/connections/pool.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { createDeferredConnection } from '../connections';
import type { ConnectorType } from '../connectors';
import {
createDeferredExecutor,
executeInNewConnection,
sqlExecutorInNewConnection,
type WithSQLExecutor,
Expand Down Expand Up @@ -72,3 +75,39 @@ export const createConnectionPool = <

return result as ConnectionPoolType;
};

export const createDeferredConnectionPool = <Connector extends ConnectorType>(
connector: Connector,
importPool: () => Promise<ConnectionPool<Connection<Connector>>>,
): ConnectionPool<Connection<Connector>> => {
let poolPromise: Promise<ConnectionPool<Connection<Connector>>> | null = null;

const getPool = async (): Promise<ConnectionPool<Connection<Connector>>> => {
if (poolPromise) return poolPromise;
try {
return (poolPromise = importPool());
} catch (error) {
throw new Error(
`Failed to import connection pool: ${error instanceof Error ? error.message : String(error)}`,
);
}
};

return createConnectionPool({
connector,
execute: createDeferredExecutor(async () => {
const connection = await getPool();
return connection.execute;
}),
close: async () => {
if (!poolPromise) return;
const pool = await poolPromise;
await pool.close();
poolPromise = null;
},
getConnection: () =>
createDeferredConnection(connector, async () =>
(await getPool()).connection(),
),
});
};
53 changes: 53 additions & 0 deletions src/packages/dumbo/src/core/execute/execute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,3 +154,56 @@ export const executeInNewConnection = async <
await connection.close();
}
};

export const createDeferredExecutor = (
importExecutor: () => Promise<SQLExecutor>,
): SQLExecutor => {
let executor: SQLExecutor | null = null;

const getExecutor = async (): Promise<SQLExecutor> => {
if (!executor) {
try {
executor = await importExecutor();
} catch (error) {
throw new Error(
`Failed to import SQL executor: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
return executor;
};

return {
query: async <Result extends QueryResultRow = QueryResultRow>(
sql: SQL,
options?: SQLQueryOptions,
): Promise<QueryResult<Result>> => {
const exec = await getExecutor();
return exec.query<Result>(sql, options);
},

batchQuery: async <Result extends QueryResultRow = QueryResultRow>(
sqls: SQL[],
options?: SQLQueryOptions,
): Promise<QueryResult<Result>[]> => {
const exec = await getExecutor();
return exec.batchQuery<Result>(sqls, options);
},

command: async <Result extends QueryResultRow = QueryResultRow>(
sql: SQL,
options?: SQLCommandOptions,
): Promise<QueryResult<Result>> => {
const exec = await getExecutor();
return exec.command<Result>(sql, options);
},

batchCommand: async <Result extends QueryResultRow = QueryResultRow>(
sqls: SQL[],
options?: SQLCommandOptions,
): Promise<QueryResult<Result>[]> => {
const exec = await getExecutor();
return exec.batchCommand<Result>(sqls, options);
},
};
};
13 changes: 8 additions & 5 deletions src/packages/dumbo/src/core/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Connection, ConnectionPool } from './connections';
import type { DatabaseConnectionString } from '../storage/all';
import { type Connection, type ConnectionPool } from './connections';
import type { ConnectorType } from './connectors';

export * from './connections';
Expand All @@ -11,11 +12,13 @@ export * from './serializer';
export * from './sql';
export * from './tracing';

export type DumboOptions<Connector extends ConnectorType = ConnectorType> = {
connector?: Connector;
};

export type Dumbo<
Connector extends ConnectorType = ConnectorType,
ConnectionType extends Connection<Connector> = Connection<Connector>,
> = ConnectionPool<ConnectionType>;

export type DumboConnectionOptions<
T extends DatabaseConnectionString = DatabaseConnectionString,
> = {
connectionString: string | T;
};
1 change: 1 addition & 0 deletions src/packages/dumbo/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './core';
export * from './storage/all';
1 change: 0 additions & 1 deletion src/packages/dumbo/src/pg.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
export * from './core';
export * from './storage/postgresql/core';
export * from './storage/postgresql/pg';
1 change: 0 additions & 1 deletion src/packages/dumbo/src/sqlite3.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
export * from './core';
export * from './storage/sqlite/core';
export * from './storage/sqlite/sqlite3';
38 changes: 38 additions & 0 deletions src/packages/dumbo/src/storage/all/connections/connectionString.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import type { ConnectorTypeParts } from '../../../core';
import type { PostgreSQLConnectionString } from '../../postgresql/core';
import type { SQLiteConnectionString } from '../../sqlite/core';

export type DatabaseConnectionString =
| PostgreSQLConnectionString
| SQLiteConnectionString;

export const parseConnectionString = (
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
connectionString: DatabaseConnectionString | string,
): ConnectorTypeParts => {
if (
connectionString.startsWith('postgresql://') ||
connectionString.startsWith('postgres://')
) {
return {
databaseType: 'PostgreSQL',
driverName: 'pg',
};
}

if (
connectionString.startsWith('file:') ||
connectionString === ':memory:' ||
connectionString.startsWith('/') ||
connectionString.startsWith('./')
) {
return {
databaseType: 'SQLite',
driverName: 'sqlite3',
};
}

throw new Error(
`Unsupported database connection string: ${connectionString}`,
);
};
1 change: 1 addition & 0 deletions src/packages/dumbo/src/storage/all/connections/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './connectionString';
76 changes: 76 additions & 0 deletions src/packages/dumbo/src/storage/all/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import {
type Connection,
type ConnectionPool,
type ConnectorType,
createDeferredConnectionPool,
type DumboConnectionOptions,
} from '../../core';
import type { PostgreSQLConnectionString } from '../postgresql/core';
import type { SQLiteConnectionString } from '../sqlite/core';
import {
type DatabaseConnectionString,
parseConnectionString,
} from './connections';

export * from './connections';

// Helper type to infer the connector type based on connection string
export type InferConnector<T extends DatabaseConnectionString> =
T extends PostgreSQLConnectionString
? ConnectorType<'PostgreSQL', 'pg'>
: T extends SQLiteConnectionString
? ConnectorType<'SQLite', 'sqlite3'>
: never;

// Helper type to infer the connection type based on connection string
export type InferConnection<T extends DatabaseConnectionString> =
T extends PostgreSQLConnectionString
? Connection<ConnectorType<'PostgreSQL', 'pg'>>
: T extends SQLiteConnectionString
? Connection<ConnectorType<'SQLite', 'sqlite3'>>
: never;

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const importDrivers: Record<string, () => Promise<any>> = {
'PostgreSQL:pg': () => import('../postgresql/pg'),
'SQLite:sqlite3': () => import('../sqlite/sqlite3'),
};

export function dumbo<
ConnectionString extends DatabaseConnectionString,
DatabaseOptions extends DumboConnectionOptions<ConnectionString>,
>(options: DatabaseOptions): ConnectionPool<InferConnection<ConnectionString>> {
const { connectionString } = options;

const { databaseType, driverName } = parseConnectionString(connectionString);

const connector: InferConnection<ConnectionString>['connector'] =
`${databaseType}:${driverName}` as InferConnection<ConnectionString>['connector'];

const importDriver = importDrivers[connector];
if (!importDriver) {
throw new Error(`Unsupported connector: ${connector}`);
}

const importAndCreatePool = async () => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const module = await importDriver();

// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const poolFactory: (options: {
connectionString: string;
}) => ConnectionPool<InferConnection<ConnectionString>> =
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
'dumbo' in module ? module.dumbo : undefined;

if (poolFactory === undefined)
throw new Error(`No pool factory found for connector: ${connector}`);

return poolFactory({ connector, ...options });
};

return createDeferredConnectionPool(
connector,
importAndCreatePool,
) as ConnectionPool<InferConnection<ConnectionString>>;
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,26 @@
import pgcs from 'pg-connection-string';
import { defaultPostgreSqlDatabase } from '../schema';

export const defaultPostgreSQLConenctionString =
'postgresql://postgres@localhost:5432/postgres';
export const defaultPostgreSQLConenctionString: PostgreSQLConnectionString =
'postgresql://postgres@localhost:5432/postgres' as PostgreSQLConnectionString;

export const getDatabaseNameOrDefault = (connectionString: string) =>
pgcs.parse(connectionString).database ?? defaultPostgreSqlDatabase;

export type PostgreSQLConnectionString =
| `postgresql://${string}`
| `postgres://${string}`;

export const PostgreSQLConnectionString = (
connectionString: string,
): PostgreSQLConnectionString => {
if (
!connectionString.startsWith('postgresql://') &&
!connectionString.startsWith('postgres://')
) {
throw new Error(
`Invalid PostgreSQL connection string: ${connectionString}. It should start with "postgresql://".`,
);
}
return connectionString as PostgreSQLConnectionString;
};
Loading
Loading