Skip to content
Open
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
40 changes: 40 additions & 0 deletions drizzle-orm/src/libsql/driver-core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,46 @@ export class LibSQLDatabase<
): Promise<BatchResponse<T>> {
return this.session.batch(batch) as Promise<BatchResponse<T>>;
}

/**
* Attach an external SQLite database file to this connection.
*
* Tables in the attached database can be queried using the schema prefix.
* Ensure the schema was defined with `sqliteSchema(name)` and included
* in the drizzle() configuration.
*
* @param schemaName - Schema name to use for queries (e.g., 'bronze')
* @param dbPath - Absolute or relative path to database file
*
* @example
* ```typescript
* const bronze = sqliteSchema('bronze');
* const messageSnapshot = bronze.table('message_snapshot', { ... });
*
* const db = drizzle(client, {
* schema: { messageSnapshot, ...warehouseSchema }
* });
*
* await db.$attach('bronze', './bronze.db');
*
* // Now queries work
* await db.select().from(messageSnapshot).all();
* ```
*/
async $attach(schemaName: string, dbPath: string): Promise<void> {
const sql = `ATTACH DATABASE '${dbPath}' AS ${schemaName}`;
await (this as any).$client.execute(sql);
}

/**
* Detach a previously attached database.
*
* @param schemaName - Schema name to detach
*/
async $detach(schemaName: string): Promise<void> {
const sql = `DETACH DATABASE ${schemaName}`;
await (this as any).$client.execute(sql);
}
}

/** @internal */
Expand Down
1 change: 1 addition & 0 deletions drizzle-orm/src/sqlite-core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export * from './foreign-keys.ts';
export * from './indexes.ts';
export * from './primary-keys.ts';
export * from './query-builders/index.ts';
export * from './schema.ts';
export * from './session.ts';
export * from './subquery.ts';
export * from './table.ts';
Expand Down
81 changes: 81 additions & 0 deletions drizzle-orm/src/sqlite-core/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { entityKind, is } from '~/entity.ts';
import { SQL, sql, type SQLWrapper } from '~/sql/sql.ts';
import { type SQLiteTableFn, sqliteTableWithSchema } from './table.ts';
import { type sqliteView, sqliteViewWithSchema } from './view.ts';

/**
* Represents a SQLite schema (database attached via ATTACH DATABASE).
*
* @example
* ```typescript
* const logs = sqliteSchema('logs');
*
* export const auditLog = logs.table('audit_log', {
* id: text('id').primaryKey(),
* action: text('action'),
* });
* ```
*/
export class SQLiteSchema<TName extends string = string> implements SQLWrapper {
static readonly [entityKind]: string = 'SQLiteSchema';

constructor(public readonly schemaName: TName) {}

/**
* Define a table in this schema.
* Queries will generate: SELECT * FROM "schemaName"."tableName"
*/
table: SQLiteTableFn<TName> = ((name, columns, extraConfig) => {
return sqliteTableWithSchema(name, columns, extraConfig, this.schemaName);
}) as SQLiteTableFn<TName>;

/**
* Define a view in this schema.
*/
view = ((name, columns) => {
return sqliteViewWithSchema(name, columns, this.schemaName);
}) as typeof sqliteView;

getSQL(): SQL {
return new SQL([sql.identifier(this.schemaName)]);
}

shouldOmitSQLParens(): boolean {
return true;
}
}

export function isSQLiteSchema(obj: unknown): obj is SQLiteSchema {
return is(obj, SQLiteSchema);
}

/**
* Define a SQLite schema for use with ATTACH DATABASE.
*
* SQLite supports attaching multiple database files to a single connection.
* Each attached database is accessed via a schema prefix.
*
* @param name - Schema name (must match ATTACH DATABASE ... AS name)
*
* @example
* ```typescript
* // 1. Define schema
* const logs = sqliteSchema('logs');
*
* export const auditLog = logs.table('audit_log', {
* id: text('id').primaryKey(),
* timestamp: integer('timestamp'),
* });
*
* // 2. Attach database
* const db = drizzle(client, { schema: { auditLog } });
* await db.$attach('logs', './logs.db');
*
* // 3. Query across databases
* await db.select().from(auditLog).all();
* // Generates: SELECT * FROM "logs"."audit_log"
* ```
*/
export function sqliteSchema<T extends string>(name: T): SQLiteSchema<T> {
return new SQLiteSchema(name);
}
23 changes: 23 additions & 0 deletions drizzle-orm/src/sqlite-core/table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,29 @@ function sqliteTableBase<
return table;
}

export function sqliteTableWithSchema<
TTableName extends string,
TSchemaName extends string | undefined,
TColumnsMap extends Record<string, SQLiteColumnBuilderBase>,
>(
name: TTableName,
columns: TColumnsMap | ((columnTypes: SQLiteColumnBuilders) => TColumnsMap),
extraConfig:
| ((
self: BuildColumns<TTableName, TColumnsMap, 'sqlite'>,
) => SQLiteTableExtraConfig | SQLiteTableExtraConfigValue[])
| undefined,
schema: TSchemaName,
baseName = name,
): SQLiteTableWithColumns<{
name: TTableName;
schema: TSchemaName;
columns: BuildColumns<TTableName, TColumnsMap, 'sqlite'>;
dialect: 'sqlite';
}> {
return sqliteTableBase(name, columns, extraConfig, schema, baseName);
}

export const sqliteTable: SQLiteTableFn = (name, columns, extraConfig) => {
return sqliteTableBase(name, columns, extraConfig);
};
Expand Down
33 changes: 29 additions & 4 deletions drizzle-orm/src/sqlite-core/view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export class ViewBuilderCore<

constructor(
protected name: TConfig['name'],
protected schema?: string,
) {}

protected config: ViewBuilderConfig = {};
Expand Down Expand Up @@ -56,7 +57,7 @@ export class ViewBuilder<TName extends string = string> extends ViewBuilderCore<
// sqliteConfig: this.config,
config: {
name: this.name,
schema: undefined,
schema: this.schema,
selectedFields: aliasedSelectedFields,
query: qb.getSQL().inlineParams(),
},
Expand All @@ -79,8 +80,9 @@ export class ManualViewBuilder<
constructor(
name: TName,
columns: TColumns,
schema?: string,
) {
super(name);
super(name, schema);
this.columns = getTableColumns(sqliteTable(name, columns)) as BuildColumns<TName, TColumns, 'sqlite'>;
}

Expand All @@ -89,7 +91,7 @@ export class ManualViewBuilder<
new SQLiteView({
config: {
name: this.name,
schema: undefined,
schema: this.schema,
selectedFields: this.columns,
query: undefined,
},
Expand All @@ -108,7 +110,7 @@ export class ManualViewBuilder<
new SQLiteView({
config: {
name: this.name,
schema: undefined,
schema: this.schema,
selectedFields: this.columns,
query: query.inlineParams(),
},
Expand Down Expand Up @@ -163,4 +165,27 @@ export function sqliteView(
return new ViewBuilder(name);
}

export function sqliteViewWithSchema<TName extends string>(
name: TName,
schema: string | undefined,
): ViewBuilder<TName>;
export function sqliteViewWithSchema<
TName extends string,
TColumns extends Record<string, SQLiteColumnBuilderBase>,
>(
name: TName,
columns: TColumns,
schema: string | undefined,
): ManualViewBuilder<TName, TColumns>;
export function sqliteViewWithSchema(
name: string,
columnsOrSchema?: Record<string, SQLiteColumnBuilderBase> | string,
schema?: string,
): ViewBuilder | ManualViewBuilder {
if (typeof columnsOrSchema === 'object') {
return new ManualViewBuilder(name, columnsOrSchema, schema);
}
return new ViewBuilder(name, columnsOrSchema as string | undefined);
}

export const view = sqliteView;
Loading