chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"detectiveOptions": {
|
||||
"ts": {
|
||||
"skipTypeImports": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
{
|
||||
"name": "drizzle-orm",
|
||||
"version": "0.45.3",
|
||||
"description": "Drizzle ORM package for SQL databases",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"p": "prisma generate --schema src/prisma/schema.prisma",
|
||||
"build": "pnpm p && scripts/build.ts",
|
||||
"b": "pnpm build",
|
||||
"test:types": "cd type-tests && tsc",
|
||||
"test": "vitest run",
|
||||
"pack": "(cd dist && npm pack --pack-destination ..) && rm -f package.tgz && mv *.tgz package.tgz",
|
||||
"publish": "npm publish package.tgz"
|
||||
},
|
||||
"main": "./index.cjs",
|
||||
"module": "./index.js",
|
||||
"types": "./index.d.ts",
|
||||
"sideEffects": false,
|
||||
"publishConfig": {
|
||||
"provenance": true
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/drizzle-team/drizzle-orm.git"
|
||||
},
|
||||
"homepage": "https://orm.drizzle.team",
|
||||
"keywords": [
|
||||
"drizzle",
|
||||
"orm",
|
||||
"pg",
|
||||
"mysql",
|
||||
"singlestore",
|
||||
"postgresql",
|
||||
"postgres",
|
||||
"sqlite",
|
||||
"database",
|
||||
"sql",
|
||||
"typescript",
|
||||
"ts",
|
||||
"drizzle-orm"
|
||||
],
|
||||
"author": "Drizzle Team",
|
||||
"license": "Apache-2.0",
|
||||
"bugs": {
|
||||
"url": "https://github.com/drizzle-team/drizzle-orm/issues"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@aws-sdk/client-rds-data": ">=3",
|
||||
"@cloudflare/workers-types": ">=4",
|
||||
"@electric-sql/pglite": ">=0.2.0",
|
||||
"@libsql/client": ">=0.10.0",
|
||||
"@libsql/client-wasm": ">=0.10.0",
|
||||
"@neondatabase/serverless": ">=0.10.0",
|
||||
"@op-engineering/op-sqlite": ">=2",
|
||||
"@opentelemetry/api": "^1.4.1",
|
||||
"@planetscale/database": ">=1.13",
|
||||
"@prisma/client": "*",
|
||||
"@tidbcloud/serverless": "*",
|
||||
"@types/better-sqlite3": "*",
|
||||
"@types/pg": "*",
|
||||
"@types/sql.js": "*",
|
||||
"@upstash/redis": ">=1.34.7",
|
||||
"@vercel/postgres": ">=0.8.0",
|
||||
"@xata.io/client": "*",
|
||||
"better-sqlite3": ">=7",
|
||||
"bun-types": "*",
|
||||
"expo-sqlite": ">=14.0.0",
|
||||
"gel": ">=2",
|
||||
"knex": "*",
|
||||
"kysely": "*",
|
||||
"mysql2": ">=2",
|
||||
"@netlify/db": ">=0.4.0",
|
||||
"pg": ">=8",
|
||||
"postgres": ">=3",
|
||||
"sql.js": ">=1",
|
||||
"sqlite3": ">=5"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"mysql2": {
|
||||
"optional": true
|
||||
},
|
||||
"@vercel/postgres": {
|
||||
"optional": true
|
||||
},
|
||||
"@xata.io/client": {
|
||||
"optional": true
|
||||
},
|
||||
"better-sqlite3": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/better-sqlite3": {
|
||||
"optional": true
|
||||
},
|
||||
"sqlite3": {
|
||||
"optional": true
|
||||
},
|
||||
"sql.js": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/sql.js": {
|
||||
"optional": true
|
||||
},
|
||||
"@cloudflare/workers-types": {
|
||||
"optional": true
|
||||
},
|
||||
"pg": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/pg": {
|
||||
"optional": true
|
||||
},
|
||||
"postgres": {
|
||||
"optional": true
|
||||
},
|
||||
"@neondatabase/serverless": {
|
||||
"optional": true
|
||||
},
|
||||
"@netlify/db": {
|
||||
"optional": true
|
||||
},
|
||||
"bun-types": {
|
||||
"optional": true
|
||||
},
|
||||
"@aws-sdk/client-rds-data": {
|
||||
"optional": true
|
||||
},
|
||||
"@planetscale/database": {
|
||||
"optional": true
|
||||
},
|
||||
"knex": {
|
||||
"optional": true
|
||||
},
|
||||
"kysely": {
|
||||
"optional": true
|
||||
},
|
||||
"@libsql/client": {
|
||||
"optional": true
|
||||
},
|
||||
"@libsql/client-wasm": {
|
||||
"optional": true
|
||||
},
|
||||
"@opentelemetry/api": {
|
||||
"optional": true
|
||||
},
|
||||
"expo-sqlite": {
|
||||
"optional": true
|
||||
},
|
||||
"gel": {
|
||||
"optional": true
|
||||
},
|
||||
"@op-engineering/op-sqlite": {
|
||||
"optional": true
|
||||
},
|
||||
"@electric-sql/pglite": {
|
||||
"optional": true
|
||||
},
|
||||
"@tidbcloud/serverless": {
|
||||
"optional": true
|
||||
},
|
||||
"prisma": {
|
||||
"optional": true
|
||||
},
|
||||
"@prisma/client": {
|
||||
"optional": true
|
||||
},
|
||||
"@upstash/redis": {
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aws-sdk/client-rds-data": "^3.549.0",
|
||||
"@cloudflare/workers-types": "^4.20241112.0",
|
||||
"@electric-sql/pglite": "^0.2.12",
|
||||
"@libsql/client": "^0.10.0",
|
||||
"@libsql/client-wasm": "^0.10.0",
|
||||
"@miniflare/d1": "^2.14.4",
|
||||
"@neondatabase/serverless": "^0.10.0",
|
||||
"@netlify/db": "^0.4.0",
|
||||
"@op-engineering/op-sqlite": "^2.0.16",
|
||||
"@opentelemetry/api": "^1.4.1",
|
||||
"@originjs/vite-plugin-commonjs": "^1.0.3",
|
||||
"@planetscale/database": "^1.16.0",
|
||||
"@prisma/client": "5.14.0",
|
||||
"@tidbcloud/serverless": "^0.1.1",
|
||||
"@types/better-sqlite3": "^7.6.12",
|
||||
"@types/node": "^20.2.5",
|
||||
"@types/pg": "^8.10.1",
|
||||
"@types/react": "^18.2.45",
|
||||
"@types/sql.js": "^1.4.4",
|
||||
"@upstash/redis": "^1.34.3",
|
||||
"@vercel/postgres": "^0.8.0",
|
||||
"@xata.io/client": "^0.29.3",
|
||||
"better-sqlite3": "^11.9.1",
|
||||
"bun-types": "^1.2.0",
|
||||
"cpy": "^10.1.0",
|
||||
"expo-sqlite": "^14.0.0",
|
||||
"gel": "^2.0.0",
|
||||
"glob": "^11.0.1",
|
||||
"knex": "^2.4.2",
|
||||
"kysely": "^0.25.0",
|
||||
"mysql2": "^3.14.1",
|
||||
"pg": "^8.11.0",
|
||||
"postgres": "^3.3.5",
|
||||
"prisma": "5.14.0",
|
||||
"react": "^18.2.0",
|
||||
"sql.js": "^1.8.0",
|
||||
"sqlite3": "^5.1.2",
|
||||
"ts-morph": "^25.0.1",
|
||||
"tslib": "^2.5.2",
|
||||
"tsx": "^3.12.7",
|
||||
"vite-tsconfig-paths": "^4.3.2",
|
||||
"vitest": "^3.1.3",
|
||||
"zod": "^3.20.2",
|
||||
"zx": "^7.2.2"
|
||||
}
|
||||
}
|
||||
Executable
+76
@@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env -S pnpm tsx
|
||||
import 'zx/globals';
|
||||
import cpy from 'cpy';
|
||||
|
||||
async function updateAndCopyPackageJson() {
|
||||
const pkg = await fs.readJSON('package.json');
|
||||
|
||||
const entries = await glob('src/**/*.ts');
|
||||
|
||||
pkg.exports = entries.reduce<
|
||||
Record<string, {
|
||||
import: {
|
||||
types?: string;
|
||||
default: string;
|
||||
};
|
||||
require: {
|
||||
types: string;
|
||||
default: string;
|
||||
};
|
||||
default: string;
|
||||
types: string;
|
||||
}>
|
||||
>(
|
||||
(acc, rawEntry) => {
|
||||
const entry = rawEntry.match(/src\/(.*)\.ts/)![1]!;
|
||||
const exportsEntry = entry === 'index' ? '.' : './' + entry.replace(/\/index$/, '');
|
||||
const importEntry = `./${entry}.js`;
|
||||
const requireEntry = `./${entry}.cjs`;
|
||||
acc[exportsEntry] = {
|
||||
import: {
|
||||
types: `./${entry}.d.ts`,
|
||||
default: importEntry,
|
||||
},
|
||||
require: {
|
||||
types: `./${entry}.d.cts`,
|
||||
default: requireEntry,
|
||||
},
|
||||
types: `./${entry}.d.ts`,
|
||||
default: importEntry,
|
||||
};
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
await fs.writeJSON('dist.new/package.json', pkg, { spaces: 2 });
|
||||
}
|
||||
|
||||
await fs.remove('dist.new');
|
||||
|
||||
await Promise.all([
|
||||
(async () => {
|
||||
await $`tsup`.stdio('pipe', 'pipe', 'pipe');
|
||||
})(),
|
||||
(async () => {
|
||||
await $`tsc -p tsconfig.dts.json`.stdio('pipe', 'pipe', 'pipe');
|
||||
await cpy('dist-dts/**/*.d.ts', 'dist.new', {
|
||||
rename: (basename) => basename.replace(/\.d\.ts$/, '.d.cts'),
|
||||
});
|
||||
await cpy('dist-dts/**/*.d.ts', 'dist.new', {
|
||||
rename: (basename) => basename.replace(/\.d\.ts$/, '.d.ts'),
|
||||
});
|
||||
})(),
|
||||
]);
|
||||
|
||||
await Promise.all([
|
||||
$`tsup src/version.ts --no-config --dts --format esm --outDir dist.new`.stdio('pipe', 'pipe', 'pipe'),
|
||||
$`tsup src/version.ts --no-config --dts --format cjs --outDir dist.new`.stdio('pipe', 'pipe', 'pipe'),
|
||||
]);
|
||||
|
||||
await $`scripts/fix-imports.ts`;
|
||||
|
||||
await fs.copy('../README.md', 'dist.new/README.md');
|
||||
await updateAndCopyPackageJson();
|
||||
await fs.remove('dist');
|
||||
await fs.rename('dist.new', 'dist');
|
||||
Executable
+101
@@ -0,0 +1,101 @@
|
||||
#!/usr/bin/env -S pnpm tsx
|
||||
import 'zx/globals';
|
||||
|
||||
import path from 'node:path';
|
||||
import { parse, print, visit } from 'recast';
|
||||
import parser from 'recast/parsers/typescript';
|
||||
|
||||
function resolvePathAlias(importPath: string, file: string) {
|
||||
if (importPath.startsWith('~/')) {
|
||||
const relativePath = path.relative(path.dirname(file), path.resolve('dist.new', importPath.slice(2)));
|
||||
importPath = relativePath.startsWith('.') ? relativePath : './' + relativePath;
|
||||
}
|
||||
|
||||
return importPath;
|
||||
}
|
||||
|
||||
function fixImportPath(importPath: string, file: string, ext: string) {
|
||||
importPath = resolvePathAlias(importPath, file);
|
||||
|
||||
if (!/\..*\.(js|ts)$/.test(importPath)) {
|
||||
return importPath;
|
||||
}
|
||||
|
||||
return importPath.replace(/\.(js|ts)$/, ext);
|
||||
}
|
||||
|
||||
const cjsFiles = await glob('dist.new/**/*.{cjs,d.cts}');
|
||||
|
||||
await Promise.all(cjsFiles.map(async (file) => {
|
||||
const code = parse(await fs.readFile(file, 'utf8'), { parser });
|
||||
|
||||
visit(code, {
|
||||
visitImportDeclaration(path) {
|
||||
path.value.source.value = fixImportPath(path.value.source.value, file, '.cjs');
|
||||
this.traverse(path);
|
||||
},
|
||||
visitExportAllDeclaration(path) {
|
||||
path.value.source.value = fixImportPath(path.value.source.value, file, '.cjs');
|
||||
this.traverse(path);
|
||||
},
|
||||
visitExportNamedDeclaration(path) {
|
||||
if (path.value.source) {
|
||||
path.value.source.value = fixImportPath(path.value.source.value, file, '.cjs');
|
||||
}
|
||||
this.traverse(path);
|
||||
},
|
||||
visitCallExpression(path) {
|
||||
if (path.value.callee.type === 'Identifier' && path.value.callee.name === 'require') {
|
||||
path.value.arguments[0].value = fixImportPath(path.value.arguments[0].value, file, '.cjs');
|
||||
}
|
||||
this.traverse(path);
|
||||
},
|
||||
visitTSImportType(path) {
|
||||
path.value.argument.value = resolvePathAlias(path.value.argument.value, file);
|
||||
this.traverse(path);
|
||||
},
|
||||
visitAwaitExpression(path) {
|
||||
if (print(path.value).code.startsWith(`await import("./`)) {
|
||||
path.value.argument.arguments[0].value = fixImportPath(path.value.argument.arguments[0].value, file, '.cjs');
|
||||
}
|
||||
this.traverse(path);
|
||||
},
|
||||
});
|
||||
|
||||
await fs.writeFile(file, print(code).code);
|
||||
}));
|
||||
|
||||
const esmFiles = await glob('dist.new/**/*.{js,d.ts}');
|
||||
|
||||
await Promise.all(esmFiles.map(async (file) => {
|
||||
const code = parse(await fs.readFile(file, 'utf8'), { parser });
|
||||
|
||||
visit(code, {
|
||||
visitImportDeclaration(path) {
|
||||
path.value.source.value = fixImportPath(path.value.source.value, file, '.js');
|
||||
this.traverse(path);
|
||||
},
|
||||
visitExportAllDeclaration(path) {
|
||||
path.value.source.value = fixImportPath(path.value.source.value, file, '.js');
|
||||
this.traverse(path);
|
||||
},
|
||||
visitExportNamedDeclaration(path) {
|
||||
if (path.value.source) {
|
||||
path.value.source.value = fixImportPath(path.value.source.value, file, '.js');
|
||||
}
|
||||
this.traverse(path);
|
||||
},
|
||||
visitTSImportType(path) {
|
||||
path.value.argument.value = fixImportPath(path.value.argument.value, file, '.js');
|
||||
this.traverse(path);
|
||||
},
|
||||
visitAwaitExpression(path) {
|
||||
if (print(path.value).code.startsWith(`await import("./`)) {
|
||||
path.value.argument.arguments[0].value = fixImportPath(path.value.argument.arguments[0].value, file, '.js');
|
||||
}
|
||||
this.traverse(path);
|
||||
},
|
||||
});
|
||||
|
||||
await fs.writeFile(file, print(code).code);
|
||||
}));
|
||||
@@ -0,0 +1,126 @@
|
||||
import type { AnyColumn } from './column.ts';
|
||||
import { Column } from './column.ts';
|
||||
import { entityKind, is } from './entity.ts';
|
||||
import type { Relation } from './relations.ts';
|
||||
import type { View } from './sql/sql.ts';
|
||||
import { SQL, sql } from './sql/sql.ts';
|
||||
import { Table } from './table.ts';
|
||||
import { ViewBaseConfig } from './view-common.ts';
|
||||
|
||||
export class ColumnAliasProxyHandler<TColumn extends Column> implements ProxyHandler<TColumn> {
|
||||
static readonly [entityKind]: string = 'ColumnAliasProxyHandler';
|
||||
|
||||
constructor(private table: Table | View) {}
|
||||
|
||||
get(columnObj: TColumn, prop: string | symbol): any {
|
||||
if (prop === 'table') {
|
||||
return this.table;
|
||||
}
|
||||
|
||||
return columnObj[prop as keyof TColumn];
|
||||
}
|
||||
}
|
||||
|
||||
export class TableAliasProxyHandler<T extends Table | View> implements ProxyHandler<T> {
|
||||
static readonly [entityKind]: string = 'TableAliasProxyHandler';
|
||||
|
||||
constructor(private alias: string, private replaceOriginalName: boolean) {}
|
||||
|
||||
get(target: T, prop: string | symbol): any {
|
||||
if (prop === Table.Symbol.IsAlias) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (prop === Table.Symbol.Name) {
|
||||
return this.alias;
|
||||
}
|
||||
|
||||
if (this.replaceOriginalName && prop === Table.Symbol.OriginalName) {
|
||||
return this.alias;
|
||||
}
|
||||
|
||||
if (prop === ViewBaseConfig) {
|
||||
return {
|
||||
...target[ViewBaseConfig as keyof typeof target],
|
||||
name: this.alias,
|
||||
isAlias: true,
|
||||
};
|
||||
}
|
||||
|
||||
if (prop === Table.Symbol.Columns) {
|
||||
const columns = (target as Table)[Table.Symbol.Columns];
|
||||
if (!columns) {
|
||||
return columns;
|
||||
}
|
||||
|
||||
const proxiedColumns: { [key: string]: any } = {};
|
||||
|
||||
Object.keys(columns).map((key) => {
|
||||
proxiedColumns[key] = new Proxy(
|
||||
columns[key]!,
|
||||
new ColumnAliasProxyHandler(new Proxy(target, this)),
|
||||
);
|
||||
});
|
||||
|
||||
return proxiedColumns;
|
||||
}
|
||||
|
||||
const value = target[prop as keyof typeof target];
|
||||
if (is(value, Column)) {
|
||||
return new Proxy(value as AnyColumn, new ColumnAliasProxyHandler(new Proxy(target, this)));
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
export class RelationTableAliasProxyHandler<T extends Relation> implements ProxyHandler<T> {
|
||||
static readonly [entityKind]: string = 'RelationTableAliasProxyHandler';
|
||||
|
||||
constructor(private alias: string) {}
|
||||
|
||||
get(target: T, prop: string | symbol): any {
|
||||
if (prop === 'sourceTable') {
|
||||
return aliasedTable(target.sourceTable, this.alias);
|
||||
}
|
||||
|
||||
return target[prop as keyof typeof target];
|
||||
}
|
||||
}
|
||||
|
||||
export function aliasedTable<T extends Table | View>(
|
||||
table: T,
|
||||
tableAlias: string,
|
||||
): T {
|
||||
return new Proxy(table, new TableAliasProxyHandler(tableAlias, false)) as any;
|
||||
}
|
||||
|
||||
export function aliasedRelation<T extends Relation>(relation: T, tableAlias: string): T {
|
||||
return new Proxy(relation, new RelationTableAliasProxyHandler(tableAlias));
|
||||
}
|
||||
|
||||
export function aliasedTableColumn<T extends AnyColumn>(column: T, tableAlias: string): T {
|
||||
return new Proxy(
|
||||
column,
|
||||
new ColumnAliasProxyHandler(new Proxy(column.table, new TableAliasProxyHandler(tableAlias, false))),
|
||||
);
|
||||
}
|
||||
|
||||
export function mapColumnsInAliasedSQLToAlias(query: SQL.Aliased, alias: string): SQL.Aliased {
|
||||
return new SQL.Aliased(mapColumnsInSQLToAlias(query.sql, alias), query.fieldAlias);
|
||||
}
|
||||
|
||||
export function mapColumnsInSQLToAlias(query: SQL, alias: string): SQL {
|
||||
return sql.join(query.queryChunks.map((c) => {
|
||||
if (is(c, Column)) {
|
||||
return aliasedTableColumn(c, alias);
|
||||
}
|
||||
if (is(c, SQL)) {
|
||||
return mapColumnsInSQLToAlias(c, alias);
|
||||
}
|
||||
if (is(c, SQL.Aliased)) {
|
||||
return mapColumnsInAliasedSQLToAlias(c, alias);
|
||||
}
|
||||
return c;
|
||||
}));
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import type { Field } from '@aws-sdk/client-rds-data';
|
||||
import { TypeHint } from '@aws-sdk/client-rds-data';
|
||||
import type { QueryTypingsValue } from '~/sql/sql.ts';
|
||||
|
||||
export function getValueFromDataApi(field: Field) {
|
||||
if (field.stringValue !== undefined) {
|
||||
return field.stringValue;
|
||||
} else if (field.booleanValue !== undefined) {
|
||||
return field.booleanValue;
|
||||
} else if (field.doubleValue !== undefined) {
|
||||
return field.doubleValue;
|
||||
} else if (field.isNull !== undefined) {
|
||||
return null;
|
||||
} else if (field.longValue !== undefined) {
|
||||
return field.longValue;
|
||||
} else if (field.blobValue !== undefined) {
|
||||
return field.blobValue;
|
||||
// eslint-disable-next-line unicorn/no-negated-condition
|
||||
} else if (field.arrayValue !== undefined) {
|
||||
if (field.arrayValue.stringValues !== undefined) {
|
||||
return field.arrayValue.stringValues;
|
||||
}
|
||||
if (field.arrayValue.longValues !== undefined) {
|
||||
return field.arrayValue.longValues;
|
||||
}
|
||||
if (field.arrayValue.doubleValues !== undefined) {
|
||||
return field.arrayValue.doubleValues;
|
||||
}
|
||||
if (field.arrayValue.booleanValues !== undefined) {
|
||||
return field.arrayValue.booleanValues;
|
||||
}
|
||||
if (field.arrayValue.arrayValues !== undefined) {
|
||||
return field.arrayValue.arrayValues;
|
||||
}
|
||||
|
||||
throw new Error('Unknown array type');
|
||||
} else {
|
||||
throw new Error('Unknown type');
|
||||
}
|
||||
}
|
||||
|
||||
export function typingsToAwsTypeHint(typings?: QueryTypingsValue): TypeHint | undefined {
|
||||
if (typings === 'date') {
|
||||
return TypeHint.DATE;
|
||||
} else if (typings === 'decimal') {
|
||||
return TypeHint.DECIMAL;
|
||||
} else if (typings === 'json') {
|
||||
return TypeHint.JSON;
|
||||
} else if (typings === 'time') {
|
||||
return TypeHint.TIME;
|
||||
} else if (typings === 'timestamp') {
|
||||
return TypeHint.TIMESTAMP;
|
||||
} else if (typings === 'uuid') {
|
||||
return TypeHint.UUID;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function toValueParam(value: any, typings?: QueryTypingsValue): { value: Field; typeHint?: TypeHint } {
|
||||
const response: { value: Field; typeHint?: TypeHint } = {
|
||||
value: {} as any,
|
||||
typeHint: typingsToAwsTypeHint(typings),
|
||||
};
|
||||
|
||||
if (value === null) {
|
||||
response.value = { isNull: true };
|
||||
} else if (typeof value === 'string') {
|
||||
switch (response.typeHint) {
|
||||
case TypeHint.DATE: {
|
||||
response.value = { stringValue: value.split('T')[0]! };
|
||||
break;
|
||||
}
|
||||
case TypeHint.TIMESTAMP: {
|
||||
response.value = { stringValue: value.replace('T', ' ').replace('Z', '') };
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
response.value = { stringValue: value };
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (typeof value === 'number' && Number.isInteger(value)) {
|
||||
response.value = { longValue: value };
|
||||
} else if (typeof value === 'number' && !Number.isInteger(value)) {
|
||||
response.value = { doubleValue: value };
|
||||
} else if (typeof value === 'boolean') {
|
||||
response.value = { booleanValue: value };
|
||||
} else if (value instanceof Date) { // eslint-disable-line no-instanceof/no-instanceof
|
||||
// TODO: check if this clause is needed? Seems like date value always comes as string
|
||||
response.value = { stringValue: value.toISOString().replace('T', ' ').replace('Z', '') };
|
||||
} else {
|
||||
throw new Error(`Unknown type for ${value}`);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
import { RDSDataClient, type RDSDataClientConfig } from '@aws-sdk/client-rds-data';
|
||||
import { entityKind, is } from '~/entity.ts';
|
||||
import type { Logger } from '~/logger.ts';
|
||||
import { DefaultLogger } from '~/logger.ts';
|
||||
import { PgDatabase } from '~/pg-core/db.ts';
|
||||
import { PgDialect } from '~/pg-core/dialect.ts';
|
||||
import type { PgColumn, PgInsertConfig, PgTable, TableConfig } from '~/pg-core/index.ts';
|
||||
import { PgArray } from '~/pg-core/index.ts';
|
||||
import type { PgRaw } from '~/pg-core/query-builders/raw.ts';
|
||||
import {
|
||||
createTableRelationsHelpers,
|
||||
extractTablesRelationalConfig,
|
||||
type RelationalSchemaConfig,
|
||||
type TablesRelationalConfig,
|
||||
} from '~/relations.ts';
|
||||
import { Param, type SQL, sql, type SQLWrapper } from '~/sql/sql.ts';
|
||||
import { Table } from '~/table.ts';
|
||||
import type { DrizzleConfig, UpdateSet } from '~/utils.ts';
|
||||
import type { AwsDataApiClient, AwsDataApiPgQueryResult, AwsDataApiPgQueryResultHKT } from './session.ts';
|
||||
import { AwsDataApiSession } from './session.ts';
|
||||
|
||||
export interface PgDriverOptions {
|
||||
logger?: Logger;
|
||||
cache?: Cache;
|
||||
database: string;
|
||||
resourceArn: string;
|
||||
secretArn: string;
|
||||
}
|
||||
|
||||
export interface DrizzleAwsDataApiPgConfig<
|
||||
TSchema extends Record<string, unknown> = Record<string, never>,
|
||||
> extends DrizzleConfig<TSchema> {
|
||||
database: string;
|
||||
resourceArn: string;
|
||||
secretArn: string;
|
||||
}
|
||||
|
||||
export class AwsDataApiPgDatabase<
|
||||
TSchema extends Record<string, unknown> = Record<string, never>,
|
||||
> extends PgDatabase<AwsDataApiPgQueryResultHKT, TSchema> {
|
||||
static override readonly [entityKind]: string = 'AwsDataApiPgDatabase';
|
||||
|
||||
override execute<
|
||||
TRow extends Record<string, unknown> = Record<string, unknown>,
|
||||
>(query: SQLWrapper | string): PgRaw<AwsDataApiPgQueryResult<TRow>> {
|
||||
return super.execute(query);
|
||||
}
|
||||
}
|
||||
|
||||
export class AwsPgDialect extends PgDialect {
|
||||
static override readonly [entityKind]: string = 'AwsPgDialect';
|
||||
|
||||
override escapeParam(num: number): string {
|
||||
return `:${num + 1}`;
|
||||
}
|
||||
|
||||
override buildInsertQuery(
|
||||
{ table, values, onConflict, returning, select, withList }: PgInsertConfig<PgTable<TableConfig>>,
|
||||
): SQL<unknown> {
|
||||
const columns: Record<string, PgColumn> = table[Table.Symbol.Columns];
|
||||
|
||||
if (!select) {
|
||||
for (const value of (values as Record<string, Param | SQL>[])) {
|
||||
for (const fieldName of Object.keys(columns)) {
|
||||
const colValue = value[fieldName];
|
||||
if (
|
||||
is(colValue, Param) && colValue.value !== undefined && is(colValue.encoder, PgArray)
|
||||
&& Array.isArray(colValue.value)
|
||||
) {
|
||||
value[fieldName] = sql`cast(${colValue} as ${sql.raw(colValue.encoder.getSQLType())})`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return super.buildInsertQuery({ table, values, onConflict, returning, withList });
|
||||
}
|
||||
|
||||
override buildUpdateSet(table: PgTable<TableConfig>, set: UpdateSet): SQL<unknown> {
|
||||
const columns: Record<string, PgColumn> = table[Table.Symbol.Columns];
|
||||
|
||||
for (const [colName, colValue] of Object.entries(set)) {
|
||||
const currentColumn = columns[colName];
|
||||
if (
|
||||
currentColumn && is(colValue, Param) && colValue.value !== undefined && is(colValue.encoder, PgArray)
|
||||
&& Array.isArray(colValue.value)
|
||||
) {
|
||||
set[colName] = sql`cast(${colValue} as ${sql.raw(colValue.encoder.getSQLType())})`;
|
||||
}
|
||||
}
|
||||
return super.buildUpdateSet(table, set);
|
||||
}
|
||||
}
|
||||
|
||||
function construct<TSchema extends Record<string, unknown> = Record<string, never>>(
|
||||
client: AwsDataApiClient,
|
||||
config: DrizzleAwsDataApiPgConfig<TSchema>,
|
||||
): AwsDataApiPgDatabase<TSchema> & {
|
||||
$client: AwsDataApiClient;
|
||||
} {
|
||||
const dialect = new AwsPgDialect({ casing: config.casing });
|
||||
let logger;
|
||||
if (config.logger === true) {
|
||||
logger = new DefaultLogger();
|
||||
} else if (config.logger !== false) {
|
||||
logger = config.logger;
|
||||
}
|
||||
|
||||
let schema: RelationalSchemaConfig<TablesRelationalConfig> | undefined;
|
||||
if (config.schema) {
|
||||
const tablesConfig = extractTablesRelationalConfig(
|
||||
config.schema,
|
||||
createTableRelationsHelpers,
|
||||
);
|
||||
schema = {
|
||||
fullSchema: config.schema,
|
||||
schema: tablesConfig.tables,
|
||||
tableNamesMap: tablesConfig.tableNamesMap,
|
||||
};
|
||||
}
|
||||
|
||||
const session = new AwsDataApiSession(client, dialect, schema, { ...config, logger, cache: config.cache }, undefined);
|
||||
const db = new AwsDataApiPgDatabase(dialect, session, schema as any);
|
||||
(<any> db).$client = client;
|
||||
(<any> db).$cache = config.cache;
|
||||
if ((<any> db).$cache) {
|
||||
(<any> db).$cache['invalidate'] = config.cache?.onMutate;
|
||||
}
|
||||
|
||||
return db as any;
|
||||
}
|
||||
|
||||
export function drizzle<
|
||||
TSchema extends Record<string, unknown> = Record<string, never>,
|
||||
TClient extends AwsDataApiClient = RDSDataClient,
|
||||
>(
|
||||
...params: [
|
||||
TClient,
|
||||
DrizzleAwsDataApiPgConfig<TSchema>,
|
||||
] | [
|
||||
(
|
||||
| (
|
||||
& DrizzleConfig<TSchema>
|
||||
& {
|
||||
connection: RDSDataClientConfig & Omit<DrizzleAwsDataApiPgConfig, keyof DrizzleConfig>;
|
||||
}
|
||||
)
|
||||
| (
|
||||
& DrizzleAwsDataApiPgConfig<TSchema>
|
||||
& {
|
||||
client: TClient;
|
||||
}
|
||||
)
|
||||
),
|
||||
]
|
||||
): AwsDataApiPgDatabase<TSchema> & {
|
||||
$client: TClient;
|
||||
} {
|
||||
// eslint-disable-next-line no-instanceof/no-instanceof
|
||||
if (params[0] instanceof RDSDataClient || params[0].constructor.name !== 'Object') {
|
||||
return construct(params[0] as TClient, params[1] as DrizzleAwsDataApiPgConfig<TSchema>) as any;
|
||||
}
|
||||
|
||||
if ((params[0] as { client?: TClient }).client) {
|
||||
const { client, ...drizzleConfig } = params[0] as {
|
||||
client: TClient;
|
||||
} & DrizzleAwsDataApiPgConfig<TSchema>;
|
||||
|
||||
return construct(client, drizzleConfig) as any;
|
||||
}
|
||||
|
||||
const { connection, ...drizzleConfig } = params[0] as {
|
||||
connection: RDSDataClientConfig & Omit<DrizzleAwsDataApiPgConfig, keyof DrizzleConfig>;
|
||||
} & DrizzleConfig<TSchema>;
|
||||
const { resourceArn, database, secretArn, ...rdsConfig } = connection;
|
||||
|
||||
const instance = new RDSDataClient(rdsConfig);
|
||||
return construct(instance, { resourceArn, database, secretArn, ...drizzleConfig }) as any;
|
||||
}
|
||||
|
||||
export namespace drizzle {
|
||||
export function mock<TSchema extends Record<string, unknown> = Record<string, never>>(
|
||||
config: DrizzleAwsDataApiPgConfig<TSchema>,
|
||||
): AwsDataApiPgDatabase<TSchema> & {
|
||||
$client: '$client is not available on drizzle.mock()';
|
||||
} {
|
||||
return construct({} as any, config) as any;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './driver.ts';
|
||||
export * from './session.ts';
|
||||
@@ -0,0 +1,11 @@
|
||||
import type { MigrationConfig } from '~/migrator.ts';
|
||||
import { readMigrationFiles } from '~/migrator.ts';
|
||||
import type { AwsDataApiPgDatabase } from './driver.ts';
|
||||
|
||||
export async function migrate<TSchema extends Record<string, unknown>>(
|
||||
db: AwsDataApiPgDatabase<TSchema>,
|
||||
config: MigrationConfig,
|
||||
) {
|
||||
const migrations = readMigrationFiles(config);
|
||||
await db.dialect.migrate(migrations, db.session, config);
|
||||
}
|
||||
@@ -0,0 +1,291 @@
|
||||
import type { ColumnMetadata, ExecuteStatementCommandOutput, Field, RDSDataClient } from '@aws-sdk/client-rds-data';
|
||||
import {
|
||||
BeginTransactionCommand,
|
||||
CommitTransactionCommand,
|
||||
ExecuteStatementCommand,
|
||||
RollbackTransactionCommand,
|
||||
} from '@aws-sdk/client-rds-data';
|
||||
import type { Cache } from '~/cache/core/cache.ts';
|
||||
import { NoopCache } from '~/cache/core/cache.ts';
|
||||
import type { WithCacheConfig } from '~/cache/core/types.ts';
|
||||
import { entityKind } from '~/entity.ts';
|
||||
import type { Logger } from '~/logger.ts';
|
||||
import {
|
||||
type PgDialect,
|
||||
PgPreparedQuery,
|
||||
type PgQueryResultHKT,
|
||||
PgSession,
|
||||
PgTransaction,
|
||||
type PgTransactionConfig,
|
||||
type PreparedQueryConfig,
|
||||
} from '~/pg-core/index.ts';
|
||||
import type { SelectedFieldsOrdered } from '~/pg-core/query-builders/select.types.ts';
|
||||
import type { RelationalSchemaConfig, TablesRelationalConfig } from '~/relations.ts';
|
||||
import { fillPlaceholders, type QueryTypingsValue, type QueryWithTypings, type SQL, sql } from '~/sql/sql.ts';
|
||||
import { mapResultRow } from '~/utils.ts';
|
||||
import { getValueFromDataApi, toValueParam } from '../common/index.ts';
|
||||
|
||||
export type AwsDataApiClient = RDSDataClient;
|
||||
|
||||
export class AwsDataApiPreparedQuery<
|
||||
T extends PreparedQueryConfig & { values: AwsDataApiPgQueryResult<unknown[]> },
|
||||
> extends PgPreparedQuery<T> {
|
||||
static override readonly [entityKind]: string = 'AwsDataApiPreparedQuery';
|
||||
|
||||
private rawQuery: ExecuteStatementCommand;
|
||||
|
||||
constructor(
|
||||
private client: AwsDataApiClient,
|
||||
private queryString: string,
|
||||
private params: unknown[],
|
||||
private typings: QueryTypingsValue[],
|
||||
private options: AwsDataApiSessionOptions,
|
||||
cache: Cache,
|
||||
queryMetadata: {
|
||||
type: 'select' | 'update' | 'delete' | 'insert';
|
||||
tables: string[];
|
||||
} | undefined,
|
||||
cacheConfig: WithCacheConfig | undefined,
|
||||
private fields: SelectedFieldsOrdered | undefined,
|
||||
/** @internal */
|
||||
readonly transactionId: string | undefined,
|
||||
private _isResponseInArrayMode: boolean,
|
||||
private customResultMapper?: (rows: unknown[][]) => T['execute'],
|
||||
) {
|
||||
super({ sql: queryString, params }, cache, queryMetadata, cacheConfig);
|
||||
this.rawQuery = new ExecuteStatementCommand({
|
||||
sql: queryString,
|
||||
parameters: [],
|
||||
secretArn: options.secretArn,
|
||||
resourceArn: options.resourceArn,
|
||||
database: options.database,
|
||||
transactionId,
|
||||
includeResultMetadata: !fields && !customResultMapper,
|
||||
});
|
||||
}
|
||||
|
||||
async execute(placeholderValues: Record<string, unknown> | undefined = {}): Promise<T['execute']> {
|
||||
const { fields, joinsNotNullableMap, customResultMapper } = this;
|
||||
|
||||
const result = await this.values(placeholderValues);
|
||||
if (!fields && !customResultMapper) {
|
||||
const { columnMetadata, rows } = result;
|
||||
if (!columnMetadata) {
|
||||
return result;
|
||||
}
|
||||
const mappedRows = rows.map((sourceRow) => {
|
||||
const row: Record<string, unknown> = {};
|
||||
for (const [index, value] of sourceRow.entries()) {
|
||||
const metadata = columnMetadata[index];
|
||||
if (!metadata) {
|
||||
throw new Error(
|
||||
`Unexpected state: no column metadata found for index ${index}. Please report this issue on GitHub: https://github.com/drizzle-team/drizzle-orm/issues/new/choose`,
|
||||
);
|
||||
}
|
||||
if (!metadata.name) {
|
||||
throw new Error(
|
||||
`Unexpected state: no column name for index ${index} found in the column metadata. Please report this issue on GitHub: https://github.com/drizzle-team/drizzle-orm/issues/new/choose`,
|
||||
);
|
||||
}
|
||||
row[metadata.name] = value;
|
||||
}
|
||||
return row;
|
||||
});
|
||||
return Object.assign(result, { rows: mappedRows });
|
||||
}
|
||||
|
||||
return customResultMapper
|
||||
? customResultMapper(result.rows!)
|
||||
: result.rows!.map((row) => mapResultRow(fields!, row, joinsNotNullableMap));
|
||||
}
|
||||
|
||||
async all(placeholderValues?: Record<string, unknown> | undefined): Promise<T['all']> {
|
||||
const result = await this.execute(placeholderValues);
|
||||
if (!this.fields && !this.customResultMapper) {
|
||||
return (result as AwsDataApiPgQueryResult<unknown>).rows;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async values(placeholderValues: Record<string, unknown> = {}): Promise<T['values']> {
|
||||
const params = fillPlaceholders(this.params, placeholderValues ?? {});
|
||||
|
||||
this.rawQuery.input.parameters = params.map((param, index) => ({
|
||||
name: `${index + 1}`,
|
||||
...toValueParam(param, this.typings[index]),
|
||||
}));
|
||||
|
||||
this.options.logger?.logQuery(this.rawQuery.input.sql!, this.rawQuery.input.parameters);
|
||||
|
||||
const result = await this.queryWithCache(this.queryString, params, async () => {
|
||||
return await this.client.send(this.rawQuery);
|
||||
});
|
||||
const rows = result.records?.map((row) => {
|
||||
return row.map((field) => getValueFromDataApi(field));
|
||||
}) ?? [];
|
||||
|
||||
return {
|
||||
...result,
|
||||
rows,
|
||||
};
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
mapResultRows(records: Field[][], columnMetadata: ColumnMetadata[]) {
|
||||
return records.map((record) => {
|
||||
const row: Record<string, unknown> = {};
|
||||
for (const [index, field] of record.entries()) {
|
||||
const { name } = columnMetadata[index]!;
|
||||
row[name ?? index] = getValueFromDataApi(field); // not what to default if name is undefined
|
||||
}
|
||||
return row;
|
||||
});
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
isResponseInArrayMode(): boolean {
|
||||
return this._isResponseInArrayMode;
|
||||
}
|
||||
}
|
||||
|
||||
export interface AwsDataApiSessionOptions {
|
||||
logger?: Logger;
|
||||
cache?: Cache;
|
||||
database: string;
|
||||
resourceArn: string;
|
||||
secretArn: string;
|
||||
}
|
||||
|
||||
interface AwsDataApiQueryBase {
|
||||
resourceArn: string;
|
||||
secretArn: string;
|
||||
database: string;
|
||||
}
|
||||
|
||||
export class AwsDataApiSession<
|
||||
TFullSchema extends Record<string, unknown>,
|
||||
TSchema extends TablesRelationalConfig,
|
||||
> extends PgSession<AwsDataApiPgQueryResultHKT, TFullSchema, TSchema> {
|
||||
static override readonly [entityKind]: string = 'AwsDataApiSession';
|
||||
|
||||
/** @internal */
|
||||
readonly rawQuery: AwsDataApiQueryBase;
|
||||
private cache: Cache;
|
||||
|
||||
constructor(
|
||||
/** @internal */
|
||||
readonly client: AwsDataApiClient,
|
||||
dialect: PgDialect,
|
||||
private schema: RelationalSchemaConfig<TSchema> | undefined,
|
||||
private options: AwsDataApiSessionOptions,
|
||||
/** @internal */
|
||||
readonly transactionId: string | undefined,
|
||||
) {
|
||||
super(dialect);
|
||||
this.rawQuery = {
|
||||
secretArn: options.secretArn,
|
||||
resourceArn: options.resourceArn,
|
||||
database: options.database,
|
||||
};
|
||||
this.cache = options.cache ?? new NoopCache();
|
||||
}
|
||||
|
||||
prepareQuery<
|
||||
T extends PreparedQueryConfig & {
|
||||
values: AwsDataApiPgQueryResult<unknown[]>;
|
||||
} = PreparedQueryConfig & {
|
||||
values: AwsDataApiPgQueryResult<unknown[]>;
|
||||
},
|
||||
>(
|
||||
query: QueryWithTypings,
|
||||
fields: SelectedFieldsOrdered | undefined,
|
||||
name: string | undefined,
|
||||
isResponseInArrayMode: boolean,
|
||||
customResultMapper?: (rows: unknown[][]) => T['execute'],
|
||||
queryMetadata?: { type: 'select' | 'update' | 'delete' | 'insert'; tables: string[] },
|
||||
cacheConfig?: WithCacheConfig,
|
||||
transactionId?: string,
|
||||
): AwsDataApiPreparedQuery<T> {
|
||||
return new AwsDataApiPreparedQuery(
|
||||
this.client,
|
||||
query.sql,
|
||||
query.params,
|
||||
query.typings ?? [],
|
||||
this.options,
|
||||
this.cache,
|
||||
queryMetadata,
|
||||
cacheConfig,
|
||||
fields,
|
||||
transactionId ?? this.transactionId,
|
||||
isResponseInArrayMode,
|
||||
customResultMapper,
|
||||
);
|
||||
}
|
||||
|
||||
override execute<T>(query: SQL): Promise<T> {
|
||||
return this.prepareQuery<PreparedQueryConfig & { execute: T; values: AwsDataApiPgQueryResult<unknown[]> }>(
|
||||
this.dialect.sqlToQuery(query),
|
||||
undefined,
|
||||
undefined,
|
||||
false,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
this.transactionId,
|
||||
).execute();
|
||||
}
|
||||
|
||||
override async transaction<T>(
|
||||
transaction: (tx: AwsDataApiTransaction<TFullSchema, TSchema>) => Promise<T>,
|
||||
config?: PgTransactionConfig | undefined,
|
||||
): Promise<T> {
|
||||
const { transactionId } = await this.client.send(new BeginTransactionCommand(this.rawQuery));
|
||||
const session = new AwsDataApiSession(this.client, this.dialect, this.schema, this.options, transactionId);
|
||||
const tx = new AwsDataApiTransaction<TFullSchema, TSchema>(this.dialect, session, this.schema);
|
||||
if (config) {
|
||||
await tx.setTransaction(config);
|
||||
}
|
||||
try {
|
||||
const result = await transaction(tx);
|
||||
await this.client.send(new CommitTransactionCommand({ ...this.rawQuery, transactionId }));
|
||||
return result;
|
||||
} catch (e) {
|
||||
await this.client.send(new RollbackTransactionCommand({ ...this.rawQuery, transactionId }));
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class AwsDataApiTransaction<
|
||||
TFullSchema extends Record<string, unknown>,
|
||||
TSchema extends TablesRelationalConfig,
|
||||
> extends PgTransaction<AwsDataApiPgQueryResultHKT, TFullSchema, TSchema> {
|
||||
static override readonly [entityKind]: string = 'AwsDataApiTransaction';
|
||||
|
||||
override async transaction<T>(
|
||||
transaction: (tx: AwsDataApiTransaction<TFullSchema, TSchema>) => Promise<T>,
|
||||
): Promise<T> {
|
||||
const savepointName = `sp${this.nestedIndex + 1}`;
|
||||
const tx = new AwsDataApiTransaction<TFullSchema, TSchema>(
|
||||
this.dialect,
|
||||
this.session,
|
||||
this.schema,
|
||||
this.nestedIndex + 1,
|
||||
);
|
||||
await this.session.execute(sql.raw(`savepoint ${savepointName}`));
|
||||
try {
|
||||
const result = await transaction(tx);
|
||||
await this.session.execute(sql.raw(`release savepoint ${savepointName}`));
|
||||
return result;
|
||||
} catch (e) {
|
||||
await this.session.execute(sql.raw(`rollback to savepoint ${savepointName}`));
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type AwsDataApiPgQueryResult<T> = ExecuteStatementCommandOutput & { rows: T[] };
|
||||
|
||||
export interface AwsDataApiPgQueryResultHKT extends PgQueryResultHKT {
|
||||
type: AwsDataApiPgQueryResult<any>;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import type { Dialect } from './column-builder.ts';
|
||||
import type { RunnableQuery } from './runnable-query.ts';
|
||||
|
||||
export type BatchItem<TDialect extends Dialect = Dialect> = RunnableQuery<any, TDialect>;
|
||||
|
||||
export type BatchResponse<T extends BatchItem[] | readonly BatchItem[]> = {
|
||||
[K in keyof T]: T[K]['_']['result'];
|
||||
};
|
||||
@@ -0,0 +1,133 @@
|
||||
import Client, { type Database, type Options, type RunResult } from 'better-sqlite3';
|
||||
import { entityKind } from '~/entity.ts';
|
||||
import { DefaultLogger } from '~/logger.ts';
|
||||
import {
|
||||
createTableRelationsHelpers,
|
||||
extractTablesRelationalConfig,
|
||||
type RelationalSchemaConfig,
|
||||
type TablesRelationalConfig,
|
||||
} from '~/relations.ts';
|
||||
import { BaseSQLiteDatabase } from '~/sqlite-core/db.ts';
|
||||
import { SQLiteSyncDialect } from '~/sqlite-core/dialect.ts';
|
||||
import { type DrizzleConfig, isConfig } from '~/utils.ts';
|
||||
import { BetterSQLiteSession } from './session.ts';
|
||||
|
||||
export type DrizzleBetterSQLite3DatabaseConfig =
|
||||
| ({
|
||||
source?:
|
||||
| string
|
||||
| Buffer;
|
||||
} & Options)
|
||||
| string
|
||||
| undefined;
|
||||
|
||||
export class BetterSQLite3Database<TSchema extends Record<string, unknown> = Record<string, never>>
|
||||
extends BaseSQLiteDatabase<'sync', RunResult, TSchema>
|
||||
{
|
||||
static override readonly [entityKind]: string = 'BetterSQLite3Database';
|
||||
}
|
||||
|
||||
function construct<TSchema extends Record<string, unknown> = Record<string, never>>(
|
||||
client: Database,
|
||||
config: Omit<DrizzleConfig<TSchema>, 'cache'> = {},
|
||||
): BetterSQLite3Database<TSchema> & {
|
||||
$client: Database;
|
||||
} {
|
||||
const dialect = new SQLiteSyncDialect({ casing: config.casing });
|
||||
let logger;
|
||||
if (config.logger === true) {
|
||||
logger = new DefaultLogger();
|
||||
} else if (config.logger !== false) {
|
||||
logger = config.logger;
|
||||
}
|
||||
|
||||
let schema: RelationalSchemaConfig<TablesRelationalConfig> | undefined;
|
||||
if (config.schema) {
|
||||
const tablesConfig = extractTablesRelationalConfig(
|
||||
config.schema,
|
||||
createTableRelationsHelpers,
|
||||
);
|
||||
schema = {
|
||||
fullSchema: config.schema,
|
||||
schema: tablesConfig.tables,
|
||||
tableNamesMap: tablesConfig.tableNamesMap,
|
||||
};
|
||||
}
|
||||
|
||||
const session = new BetterSQLiteSession(client, dialect, schema, { logger });
|
||||
const db = new BetterSQLite3Database('sync', dialect, session, schema);
|
||||
(<any> db).$client = client;
|
||||
// (<any> db).$cache = config.cache;
|
||||
// if ((<any> db).$cache) {
|
||||
// (<any> db).$cache['invalidate'] = config.cache?.onMutate;
|
||||
// }
|
||||
|
||||
return db as any;
|
||||
}
|
||||
|
||||
export function drizzle<
|
||||
TSchema extends Record<string, unknown> = Record<string, never>,
|
||||
>(
|
||||
...params:
|
||||
| []
|
||||
| [
|
||||
Database | string,
|
||||
]
|
||||
| [
|
||||
Database | string,
|
||||
DrizzleConfig<TSchema>,
|
||||
]
|
||||
| [
|
||||
(
|
||||
& DrizzleConfig<TSchema>
|
||||
& ({
|
||||
connection?: DrizzleBetterSQLite3DatabaseConfig;
|
||||
} | {
|
||||
client: Database;
|
||||
})
|
||||
),
|
||||
]
|
||||
): BetterSQLite3Database<TSchema> & {
|
||||
$client: Database;
|
||||
} {
|
||||
if (params[0] === undefined || typeof params[0] === 'string') {
|
||||
const instance = params[0] === undefined ? new Client() : new Client(params[0]);
|
||||
|
||||
return construct(instance, params[1]) as any;
|
||||
}
|
||||
|
||||
if (isConfig(params[0])) {
|
||||
const { connection, client, ...drizzleConfig } = params[0] as
|
||||
& {
|
||||
connection?: DrizzleBetterSQLite3DatabaseConfig;
|
||||
client?: Database;
|
||||
}
|
||||
& DrizzleConfig<TSchema>;
|
||||
|
||||
if (client) return construct(client, drizzleConfig) as any;
|
||||
|
||||
if (typeof connection === 'object') {
|
||||
const { source, ...options } = connection;
|
||||
|
||||
const instance = new Client(source, options);
|
||||
|
||||
return construct(instance, drizzleConfig) as any;
|
||||
}
|
||||
|
||||
const instance = new Client(connection);
|
||||
|
||||
return construct(instance, drizzleConfig) as any;
|
||||
}
|
||||
|
||||
return construct(params[0] as Database, params[1] as DrizzleConfig<TSchema> | undefined) as any;
|
||||
}
|
||||
|
||||
export namespace drizzle {
|
||||
export function mock<TSchema extends Record<string, unknown> = Record<string, never>>(
|
||||
config?: DrizzleConfig<TSchema>,
|
||||
): BetterSQLite3Database<TSchema> & {
|
||||
$client: '$client is not available on drizzle.mock()';
|
||||
} {
|
||||
return construct({} as any, config) as any;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './driver.ts';
|
||||
export * from './session.ts';
|
||||
@@ -0,0 +1,11 @@
|
||||
import type { MigrationConfig } from '~/migrator.ts';
|
||||
import { readMigrationFiles } from '~/migrator.ts';
|
||||
import type { BetterSQLite3Database } from './driver.ts';
|
||||
|
||||
export function migrate<TSchema extends Record<string, unknown>>(
|
||||
db: BetterSQLite3Database<TSchema>,
|
||||
config: MigrationConfig,
|
||||
) {
|
||||
const migrations = readMigrationFiles(config);
|
||||
db.dialect.migrate(migrations, db.session, config);
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
import type { Database, RunResult, Statement } from 'better-sqlite3';
|
||||
import { type Cache, NoopCache } from '~/cache/core/index.ts';
|
||||
import type { WithCacheConfig } from '~/cache/core/types.ts';
|
||||
import { entityKind } from '~/entity.ts';
|
||||
import type { Logger } from '~/logger.ts';
|
||||
import { NoopLogger } from '~/logger.ts';
|
||||
import type { RelationalSchemaConfig, TablesRelationalConfig } from '~/relations.ts';
|
||||
import { fillPlaceholders, type Query, sql } from '~/sql/sql.ts';
|
||||
import type { SQLiteSyncDialect } from '~/sqlite-core/dialect.ts';
|
||||
import { SQLiteTransaction } from '~/sqlite-core/index.ts';
|
||||
import type { SelectedFieldsOrdered } from '~/sqlite-core/query-builders/select.types.ts';
|
||||
import {
|
||||
type PreparedQueryConfig as PreparedQueryConfigBase,
|
||||
type SQLiteExecuteMethod,
|
||||
SQLitePreparedQuery as PreparedQueryBase,
|
||||
SQLiteSession,
|
||||
type SQLiteTransactionConfig,
|
||||
} from '~/sqlite-core/session.ts';
|
||||
import { mapResultRow } from '~/utils.ts';
|
||||
|
||||
export interface BetterSQLiteSessionOptions {
|
||||
logger?: Logger;
|
||||
cache?: Cache;
|
||||
}
|
||||
|
||||
type PreparedQueryConfig = Omit<PreparedQueryConfigBase, 'statement' | 'run'>;
|
||||
|
||||
export class BetterSQLiteSession<
|
||||
TFullSchema extends Record<string, unknown>,
|
||||
TSchema extends TablesRelationalConfig,
|
||||
> extends SQLiteSession<'sync', RunResult, TFullSchema, TSchema> {
|
||||
static override readonly [entityKind]: string = 'BetterSQLiteSession';
|
||||
|
||||
private logger: Logger;
|
||||
private cache: Cache;
|
||||
|
||||
constructor(
|
||||
private client: Database,
|
||||
dialect: SQLiteSyncDialect,
|
||||
private schema: RelationalSchemaConfig<TSchema> | undefined,
|
||||
options: BetterSQLiteSessionOptions = {},
|
||||
) {
|
||||
super(dialect);
|
||||
this.logger = options.logger ?? new NoopLogger();
|
||||
this.cache = options.cache ?? new NoopCache();
|
||||
}
|
||||
|
||||
prepareQuery<T extends Omit<PreparedQueryConfig, 'run'>>(
|
||||
query: Query,
|
||||
fields: SelectedFieldsOrdered | undefined,
|
||||
executeMethod: SQLiteExecuteMethod,
|
||||
isResponseInArrayMode: boolean,
|
||||
customResultMapper?: (rows: unknown[][]) => unknown,
|
||||
queryMetadata?: {
|
||||
type: 'select' | 'update' | 'delete' | 'insert';
|
||||
tables: string[];
|
||||
},
|
||||
cacheConfig?: WithCacheConfig,
|
||||
): PreparedQuery<T> {
|
||||
const stmt = this.client.prepare(query.sql);
|
||||
return new PreparedQuery(
|
||||
stmt,
|
||||
query,
|
||||
this.logger,
|
||||
this.cache,
|
||||
queryMetadata,
|
||||
cacheConfig,
|
||||
fields,
|
||||
executeMethod,
|
||||
isResponseInArrayMode,
|
||||
customResultMapper,
|
||||
);
|
||||
}
|
||||
|
||||
override transaction<T>(
|
||||
transaction: (tx: BetterSQLiteTransaction<TFullSchema, TSchema>) => T,
|
||||
config: SQLiteTransactionConfig = {},
|
||||
): T {
|
||||
const tx = new BetterSQLiteTransaction('sync', this.dialect, this, this.schema);
|
||||
const nativeTx = this.client.transaction(transaction);
|
||||
return nativeTx[config.behavior ?? 'deferred'](tx);
|
||||
}
|
||||
}
|
||||
|
||||
export class BetterSQLiteTransaction<
|
||||
TFullSchema extends Record<string, unknown>,
|
||||
TSchema extends TablesRelationalConfig,
|
||||
> extends SQLiteTransaction<'sync', RunResult, TFullSchema, TSchema> {
|
||||
static override readonly [entityKind]: string = 'BetterSQLiteTransaction';
|
||||
|
||||
override transaction<T>(transaction: (tx: BetterSQLiteTransaction<TFullSchema, TSchema>) => T): T {
|
||||
const savepointName = `sp${this.nestedIndex}`;
|
||||
const tx = new BetterSQLiteTransaction('sync', this.dialect, this.session, this.schema, this.nestedIndex + 1);
|
||||
this.session.run(sql.raw(`savepoint ${savepointName}`));
|
||||
try {
|
||||
const result = transaction(tx);
|
||||
this.session.run(sql.raw(`release savepoint ${savepointName}`));
|
||||
return result;
|
||||
} catch (err) {
|
||||
this.session.run(sql.raw(`rollback to savepoint ${savepointName}`));
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class PreparedQuery<T extends PreparedQueryConfig = PreparedQueryConfig> extends PreparedQueryBase<
|
||||
{ type: 'sync'; run: RunResult; all: T['all']; get: T['get']; values: T['values']; execute: T['execute'] }
|
||||
> {
|
||||
static override readonly [entityKind]: string = 'BetterSQLitePreparedQuery';
|
||||
|
||||
constructor(
|
||||
private stmt: Statement,
|
||||
query: Query,
|
||||
private logger: Logger,
|
||||
cache: Cache,
|
||||
queryMetadata: {
|
||||
type: 'select' | 'update' | 'delete' | 'insert';
|
||||
tables: string[];
|
||||
} | undefined,
|
||||
cacheConfig: WithCacheConfig | undefined,
|
||||
private fields: SelectedFieldsOrdered | undefined,
|
||||
executeMethod: SQLiteExecuteMethod,
|
||||
private _isResponseInArrayMode: boolean,
|
||||
private customResultMapper?: (rows: unknown[][]) => unknown,
|
||||
) {
|
||||
super('sync', executeMethod, query, cache, queryMetadata, cacheConfig);
|
||||
}
|
||||
|
||||
run(placeholderValues?: Record<string, unknown>): RunResult {
|
||||
const params = fillPlaceholders(this.query.params, placeholderValues ?? {});
|
||||
this.logger.logQuery(this.query.sql, params);
|
||||
return this.stmt.run(...params);
|
||||
}
|
||||
|
||||
all(placeholderValues?: Record<string, unknown>): T['all'] {
|
||||
const { fields, joinsNotNullableMap, query, logger, stmt, customResultMapper } = this;
|
||||
if (!fields && !customResultMapper) {
|
||||
const params = fillPlaceholders(query.params, placeholderValues ?? {});
|
||||
logger.logQuery(query.sql, params);
|
||||
return stmt.all(...params);
|
||||
}
|
||||
|
||||
const rows = this.values(placeholderValues) as unknown[][];
|
||||
if (customResultMapper) {
|
||||
return customResultMapper(rows) as T['all'];
|
||||
}
|
||||
return rows.map((row) => mapResultRow(fields!, row, joinsNotNullableMap));
|
||||
}
|
||||
|
||||
get(placeholderValues?: Record<string, unknown>): T['get'] {
|
||||
const params = fillPlaceholders(this.query.params, placeholderValues ?? {});
|
||||
this.logger.logQuery(this.query.sql, params);
|
||||
|
||||
const { fields, stmt, joinsNotNullableMap, customResultMapper } = this;
|
||||
if (!fields && !customResultMapper) {
|
||||
return stmt.get(...params);
|
||||
}
|
||||
|
||||
const row = stmt.raw().get(...params) as unknown[];
|
||||
|
||||
if (!row) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (customResultMapper) {
|
||||
return customResultMapper([row]) as T['get'];
|
||||
}
|
||||
|
||||
return mapResultRow(fields!, row, joinsNotNullableMap);
|
||||
}
|
||||
|
||||
values(placeholderValues?: Record<string, unknown>): T['values'] {
|
||||
const params = fillPlaceholders(this.query.params, placeholderValues ?? {});
|
||||
this.logger.logQuery(this.query.sql, params);
|
||||
return this.stmt.raw().all(...params) as T['values'];
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
isResponseInArrayMode(): boolean {
|
||||
return this._isResponseInArrayMode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
/// <reference types="bun-types" />
|
||||
|
||||
import type { SQLOptions } from 'bun';
|
||||
import { SQL } from 'bun';
|
||||
import { entityKind } from '~/entity.ts';
|
||||
import { DefaultLogger } from '~/logger.ts';
|
||||
import { PgDatabase } from '~/pg-core/db.ts';
|
||||
import { PgDialect } from '~/pg-core/dialect.ts';
|
||||
import {
|
||||
createTableRelationsHelpers,
|
||||
extractTablesRelationalConfig,
|
||||
type RelationalSchemaConfig,
|
||||
type TablesRelationalConfig,
|
||||
} from '~/relations.ts';
|
||||
import { type DrizzleConfig, isConfig } from '~/utils.ts';
|
||||
import type { BunSQLQueryResultHKT } from './session.ts';
|
||||
import { BunSQLSession } from './session.ts';
|
||||
|
||||
export class BunSQLDatabase<
|
||||
TSchema extends Record<string, unknown> = Record<string, never>,
|
||||
> extends PgDatabase<BunSQLQueryResultHKT, TSchema> {
|
||||
static override readonly [entityKind]: string = 'BunSQLDatabase';
|
||||
}
|
||||
|
||||
function construct<TSchema extends Record<string, unknown> = Record<string, never>>(
|
||||
client: SQL,
|
||||
config: DrizzleConfig<TSchema> = {},
|
||||
): BunSQLDatabase<TSchema> & {
|
||||
$client: SQL;
|
||||
} {
|
||||
const dialect = new PgDialect({ casing: config.casing });
|
||||
let logger;
|
||||
if (config.logger === true) {
|
||||
logger = new DefaultLogger();
|
||||
} else if (config.logger !== false) {
|
||||
logger = config.logger;
|
||||
}
|
||||
|
||||
let schema: RelationalSchemaConfig<TablesRelationalConfig> | undefined;
|
||||
if (config.schema) {
|
||||
const tablesConfig = extractTablesRelationalConfig(
|
||||
config.schema,
|
||||
createTableRelationsHelpers,
|
||||
);
|
||||
schema = {
|
||||
fullSchema: config.schema,
|
||||
schema: tablesConfig.tables,
|
||||
tableNamesMap: tablesConfig.tableNamesMap,
|
||||
};
|
||||
}
|
||||
|
||||
const session = new BunSQLSession(client, dialect, schema, { logger, cache: config.cache });
|
||||
const db = new BunSQLDatabase(dialect, session, schema as any) as BunSQLDatabase<TSchema>;
|
||||
(<any> db).$client = client;
|
||||
(<any> db).$cache = config.cache;
|
||||
if ((<any> db).$cache) {
|
||||
(<any> db).$cache['invalidate'] = config.cache?.onMutate;
|
||||
}
|
||||
|
||||
return db as any;
|
||||
}
|
||||
|
||||
export function drizzle<
|
||||
TSchema extends Record<string, unknown> = Record<string, never>,
|
||||
TClient extends SQL = SQL,
|
||||
>(
|
||||
...params: [
|
||||
TClient | string,
|
||||
] | [
|
||||
TClient | string,
|
||||
DrizzleConfig<TSchema>,
|
||||
] | [
|
||||
(
|
||||
& DrizzleConfig<TSchema>
|
||||
& ({
|
||||
connection: string | ({ url?: string } & SQLOptions);
|
||||
} | {
|
||||
client: TClient;
|
||||
})
|
||||
),
|
||||
]
|
||||
): BunSQLDatabase<TSchema> & {
|
||||
$client: TClient;
|
||||
} {
|
||||
if (typeof params[0] === 'string') {
|
||||
const instance = new SQL(params[0]);
|
||||
|
||||
return construct(instance, params[1]) as any;
|
||||
}
|
||||
|
||||
if (isConfig(params[0])) {
|
||||
const { connection, client, ...drizzleConfig } = params[0] as {
|
||||
connection?: { url?: string } & SQLOptions;
|
||||
client?: TClient;
|
||||
} & DrizzleConfig<TSchema>;
|
||||
|
||||
if (client) return construct(client, drizzleConfig) as any;
|
||||
|
||||
if (typeof connection === 'object' && connection.url !== undefined) {
|
||||
const { url, ...config } = connection;
|
||||
|
||||
const instance = new SQL({ url, ...config });
|
||||
return construct(instance, drizzleConfig) as any;
|
||||
}
|
||||
|
||||
const instance = new SQL(connection);
|
||||
return construct(instance, drizzleConfig) as any;
|
||||
}
|
||||
|
||||
return construct(params[0] as TClient, params[1] as DrizzleConfig<TSchema> | undefined) as any;
|
||||
}
|
||||
|
||||
export namespace drizzle {
|
||||
export function mock<TSchema extends Record<string, unknown> = Record<string, never>>(
|
||||
config?: DrizzleConfig<TSchema>,
|
||||
): BunSQLDatabase<TSchema> & {
|
||||
$client: '$client is not available on drizzle.mock()';
|
||||
} {
|
||||
return construct({
|
||||
options: {
|
||||
parsers: {},
|
||||
serializers: {},
|
||||
},
|
||||
} as any, config) as any;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './driver.ts';
|
||||
export * from './session.ts';
|
||||
@@ -0,0 +1,11 @@
|
||||
import type { MigrationConfig } from '~/migrator.ts';
|
||||
import { readMigrationFiles } from '~/migrator.ts';
|
||||
import type { BunSQLDatabase } from './driver.ts';
|
||||
|
||||
export async function migrate<TSchema extends Record<string, unknown>>(
|
||||
db: BunSQLDatabase<TSchema>,
|
||||
config: MigrationConfig,
|
||||
) {
|
||||
const migrations = readMigrationFiles(config);
|
||||
await db.dialect.migrate(migrations, db.session, config);
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
/// <reference types="bun-types" />
|
||||
|
||||
import type { SavepointSQL, SQL, TransactionSQL } from 'bun';
|
||||
import { type Cache, NoopCache } from '~/cache/core/index.ts';
|
||||
import type { WithCacheConfig } from '~/cache/core/types.ts';
|
||||
import { entityKind } from '~/entity.ts';
|
||||
import type { Logger } from '~/logger.ts';
|
||||
import { NoopLogger } from '~/logger.ts';
|
||||
import type { PgDialect } from '~/pg-core/dialect.ts';
|
||||
import { PgTransaction } from '~/pg-core/index.ts';
|
||||
import type { SelectedFieldsOrdered } from '~/pg-core/query-builders/select.types.ts';
|
||||
import type { PgQueryResultHKT, PgTransactionConfig, PreparedQueryConfig } from '~/pg-core/session.ts';
|
||||
import { PgPreparedQuery, PgSession } from '~/pg-core/session.ts';
|
||||
import type { RelationalSchemaConfig, TablesRelationalConfig } from '~/relations.ts';
|
||||
import { fillPlaceholders, type Query } from '~/sql/sql.ts';
|
||||
import { tracer } from '~/tracing.ts';
|
||||
import { type Assume, mapResultRow } from '~/utils.ts';
|
||||
|
||||
export class BunSQLPreparedQuery<T extends PreparedQueryConfig> extends PgPreparedQuery<T> {
|
||||
static override readonly [entityKind]: string = 'BunSQLPreparedQuery';
|
||||
|
||||
constructor(
|
||||
private client: SQL,
|
||||
private queryString: string,
|
||||
private params: unknown[],
|
||||
private logger: Logger,
|
||||
cache: Cache,
|
||||
queryMetadata: {
|
||||
type: 'select' | 'update' | 'delete' | 'insert';
|
||||
tables: string[];
|
||||
} | undefined,
|
||||
cacheConfig: WithCacheConfig | undefined,
|
||||
private fields: SelectedFieldsOrdered | undefined,
|
||||
private _isResponseInArrayMode: boolean,
|
||||
private customResultMapper?: (rows: unknown[][]) => T['execute'],
|
||||
) {
|
||||
super({ sql: queryString, params }, cache, queryMetadata, cacheConfig);
|
||||
}
|
||||
|
||||
async execute(placeholderValues: Record<string, unknown> | undefined = {}): Promise<T['execute']> {
|
||||
return tracer.startActiveSpan('drizzle.execute', async (span) => {
|
||||
const params = fillPlaceholders(this.params, placeholderValues);
|
||||
|
||||
span?.setAttributes({
|
||||
'drizzle.query.text': this.queryString,
|
||||
'drizzle.query.params': JSON.stringify(params),
|
||||
});
|
||||
|
||||
this.logger.logQuery(this.queryString, params);
|
||||
|
||||
const { fields, queryString: query, client, joinsNotNullableMap, customResultMapper } = this;
|
||||
if (!fields && !customResultMapper) {
|
||||
return tracer.startActiveSpan('drizzle.driver.execute', async () => {
|
||||
return await this.queryWithCache(query, params, async () => {
|
||||
return await client.unsafe(query, params as any[]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const rows: any[] = await tracer.startActiveSpan('drizzle.driver.execute', async () => {
|
||||
span?.setAttributes({
|
||||
'drizzle.query.text': query,
|
||||
'drizzle.query.params': JSON.stringify(params),
|
||||
});
|
||||
|
||||
return await this.queryWithCache(query, params, async () => {
|
||||
return client.unsafe(query, params as any[]).values();
|
||||
});
|
||||
});
|
||||
|
||||
return tracer.startActiveSpan('drizzle.mapResponse', () => {
|
||||
return customResultMapper
|
||||
? customResultMapper(rows)
|
||||
: rows.map((row) => mapResultRow<T['execute']>(fields!, row, joinsNotNullableMap));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
all(placeholderValues: Record<string, unknown> | undefined = {}): Promise<T['all']> {
|
||||
return tracer.startActiveSpan('drizzle.execute', async (span) => {
|
||||
const params = fillPlaceholders(this.params, placeholderValues);
|
||||
span?.setAttributes({
|
||||
'drizzle.query.text': this.queryString,
|
||||
'drizzle.query.params': JSON.stringify(params),
|
||||
});
|
||||
this.logger.logQuery(this.queryString, params);
|
||||
return tracer.startActiveSpan('drizzle.driver.execute', async () => {
|
||||
span?.setAttributes({
|
||||
'drizzle.query.text': this.queryString,
|
||||
'drizzle.query.params': JSON.stringify(params),
|
||||
});
|
||||
return await this.queryWithCache(this.queryString, params, async () => {
|
||||
return await this.client.unsafe(this.queryString, params as any[]);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
isResponseInArrayMode(): boolean {
|
||||
return this._isResponseInArrayMode;
|
||||
}
|
||||
}
|
||||
|
||||
export interface BunSQLSessionOptions {
|
||||
logger?: Logger;
|
||||
cache?: Cache;
|
||||
}
|
||||
|
||||
export class BunSQLSession<
|
||||
TSQL extends SQL,
|
||||
TFullSchema extends Record<string, unknown>,
|
||||
TSchema extends TablesRelationalConfig,
|
||||
> extends PgSession<BunSQLQueryResultHKT, TFullSchema, TSchema> {
|
||||
static override readonly [entityKind]: string = 'BunSQLSession';
|
||||
|
||||
logger: Logger;
|
||||
private cache: Cache;
|
||||
|
||||
constructor(
|
||||
public client: TSQL,
|
||||
dialect: PgDialect,
|
||||
private schema: RelationalSchemaConfig<TSchema> | undefined,
|
||||
/** @internal */
|
||||
readonly options: BunSQLSessionOptions = {},
|
||||
) {
|
||||
super(dialect);
|
||||
this.logger = options.logger ?? new NoopLogger();
|
||||
this.cache = options.cache ?? new NoopCache();
|
||||
}
|
||||
|
||||
prepareQuery<T extends PreparedQueryConfig = PreparedQueryConfig>(
|
||||
query: Query,
|
||||
fields: SelectedFieldsOrdered | undefined,
|
||||
name: string | undefined,
|
||||
isResponseInArrayMode: boolean,
|
||||
customResultMapper?: (rows: unknown[][]) => T['execute'],
|
||||
queryMetadata?: {
|
||||
type: 'select' | 'update' | 'delete' | 'insert';
|
||||
tables: string[];
|
||||
},
|
||||
cacheConfig?: WithCacheConfig,
|
||||
): PgPreparedQuery<T> {
|
||||
return new BunSQLPreparedQuery(
|
||||
this.client,
|
||||
query.sql,
|
||||
query.params,
|
||||
this.logger,
|
||||
this.cache,
|
||||
queryMetadata,
|
||||
cacheConfig,
|
||||
fields,
|
||||
isResponseInArrayMode,
|
||||
customResultMapper,
|
||||
);
|
||||
}
|
||||
|
||||
query(query: string, params: unknown[]): Promise<any> {
|
||||
this.logger.logQuery(query, params);
|
||||
return this.client.unsafe(query, params as any[]).values();
|
||||
}
|
||||
|
||||
queryObjects(
|
||||
query: string,
|
||||
params: unknown[],
|
||||
): Promise<any> {
|
||||
return this.client.unsafe(query, params as any[]);
|
||||
}
|
||||
|
||||
override transaction<T>(
|
||||
transaction: (tx: BunSQLTransaction<TFullSchema, TSchema>) => Promise<T>,
|
||||
config?: PgTransactionConfig,
|
||||
): Promise<T> {
|
||||
return this.client.begin(async (client) => {
|
||||
const session = new BunSQLSession<TransactionSQL, TFullSchema, TSchema>(
|
||||
client,
|
||||
this.dialect,
|
||||
this.schema,
|
||||
this.options,
|
||||
);
|
||||
const tx = new BunSQLTransaction(this.dialect, session, this.schema);
|
||||
if (config) {
|
||||
await tx.setTransaction(config);
|
||||
}
|
||||
return transaction(tx);
|
||||
}) as Promise<T>;
|
||||
}
|
||||
}
|
||||
|
||||
export class BunSQLTransaction<
|
||||
TFullSchema extends Record<string, unknown>,
|
||||
TSchema extends TablesRelationalConfig,
|
||||
> extends PgTransaction<BunSQLQueryResultHKT, TFullSchema, TSchema> {
|
||||
static override readonly [entityKind]: string = 'BunSQLTransaction';
|
||||
|
||||
constructor(
|
||||
dialect: PgDialect,
|
||||
/** @internal */
|
||||
override readonly session: BunSQLSession<TransactionSQL | SavepointSQL, TFullSchema, TSchema>,
|
||||
schema: RelationalSchemaConfig<TSchema> | undefined,
|
||||
nestedIndex = 0,
|
||||
) {
|
||||
super(dialect, session, schema, nestedIndex);
|
||||
}
|
||||
|
||||
override transaction<T>(
|
||||
transaction: (tx: BunSQLTransaction<TFullSchema, TSchema>) => Promise<T>,
|
||||
): Promise<T> {
|
||||
return (this.session.client as TransactionSQL).savepoint((client: SQL) => {
|
||||
const session = new BunSQLSession<SavepointSQL, TFullSchema, TSchema>(
|
||||
client,
|
||||
this.dialect,
|
||||
this.schema,
|
||||
this.session.options,
|
||||
);
|
||||
const tx = new BunSQLTransaction<TFullSchema, TSchema>(this.dialect, session, this.schema);
|
||||
return transaction(tx);
|
||||
}) as Promise<T>;
|
||||
}
|
||||
}
|
||||
|
||||
export interface BunSQLQueryResultHKT extends PgQueryResultHKT {
|
||||
type: Assume<this['row'], Record<string, any>[]>;
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
/// <reference types="bun-types" />
|
||||
|
||||
import { Database } from 'bun:sqlite';
|
||||
import { entityKind } from '~/entity.ts';
|
||||
import { DefaultLogger } from '~/logger.ts';
|
||||
import {
|
||||
createTableRelationsHelpers,
|
||||
extractTablesRelationalConfig,
|
||||
type RelationalSchemaConfig,
|
||||
type TablesRelationalConfig,
|
||||
} from '~/relations.ts';
|
||||
import { BaseSQLiteDatabase } from '~/sqlite-core/db.ts';
|
||||
import { SQLiteSyncDialect } from '~/sqlite-core/dialect.ts';
|
||||
import { type DrizzleConfig, isConfig } from '~/utils.ts';
|
||||
import { SQLiteBunSession } from './session.ts';
|
||||
|
||||
export class BunSQLiteDatabase<
|
||||
TSchema extends Record<string, unknown> = Record<string, never>,
|
||||
> extends BaseSQLiteDatabase<'sync', void, TSchema> {
|
||||
static override readonly [entityKind]: string = 'BunSQLiteDatabase';
|
||||
}
|
||||
|
||||
type DrizzleBunSqliteDatabaseOptions = {
|
||||
/**
|
||||
* Open the database as read-only (no write operations, no create).
|
||||
*
|
||||
* Equivalent to {@link constants.SQLITE_OPEN_READONLY}
|
||||
*/
|
||||
readonly?: boolean;
|
||||
/**
|
||||
* Allow creating a new database
|
||||
*
|
||||
* Equivalent to {@link constants.SQLITE_OPEN_CREATE}
|
||||
*/
|
||||
create?: boolean;
|
||||
/**
|
||||
* Open the database as read-write
|
||||
*
|
||||
* Equivalent to {@link constants.SQLITE_OPEN_READWRITE}
|
||||
*/
|
||||
readwrite?: boolean;
|
||||
};
|
||||
|
||||
export type DrizzleBunSqliteDatabaseConfig =
|
||||
| ({
|
||||
source?: string;
|
||||
} & DrizzleBunSqliteDatabaseOptions)
|
||||
| string
|
||||
| undefined;
|
||||
|
||||
function construct<TSchema extends Record<string, unknown> = Record<string, never>>(
|
||||
client: Database,
|
||||
config: DrizzleConfig<TSchema> = {},
|
||||
): BunSQLiteDatabase<TSchema> & {
|
||||
$client: Database;
|
||||
} {
|
||||
const dialect = new SQLiteSyncDialect({ casing: config.casing });
|
||||
let logger;
|
||||
if (config.logger === true) {
|
||||
logger = new DefaultLogger();
|
||||
} else if (config.logger !== false) {
|
||||
logger = config.logger;
|
||||
}
|
||||
|
||||
let schema: RelationalSchemaConfig<TablesRelationalConfig> | undefined;
|
||||
if (config.schema) {
|
||||
const tablesConfig = extractTablesRelationalConfig(
|
||||
config.schema,
|
||||
createTableRelationsHelpers,
|
||||
);
|
||||
schema = {
|
||||
fullSchema: config.schema,
|
||||
schema: tablesConfig.tables,
|
||||
tableNamesMap: tablesConfig.tableNamesMap,
|
||||
};
|
||||
}
|
||||
|
||||
const session = new SQLiteBunSession(client, dialect, schema, { logger });
|
||||
const db = new BunSQLiteDatabase('sync', dialect, session, schema) as BunSQLiteDatabase<TSchema>;
|
||||
(<any> db).$client = client;
|
||||
|
||||
return db as any;
|
||||
}
|
||||
|
||||
export function drizzle<
|
||||
TSchema extends Record<string, unknown> = Record<string, never>,
|
||||
TClient extends Database = Database,
|
||||
>(
|
||||
...params:
|
||||
| []
|
||||
| [
|
||||
TClient | string,
|
||||
]
|
||||
| [
|
||||
TClient | string,
|
||||
DrizzleConfig<TSchema>,
|
||||
]
|
||||
| [
|
||||
(
|
||||
& DrizzleConfig<TSchema>
|
||||
& ({
|
||||
connection?: DrizzleBunSqliteDatabaseConfig;
|
||||
} | {
|
||||
client: TClient;
|
||||
})
|
||||
),
|
||||
]
|
||||
): BunSQLiteDatabase<TSchema> & {
|
||||
$client: TClient;
|
||||
} {
|
||||
if (params[0] === undefined || typeof params[0] === 'string') {
|
||||
const instance = params[0] === undefined ? new Database() : new Database(params[0]);
|
||||
|
||||
return construct(instance, params[1]) as any;
|
||||
}
|
||||
|
||||
if (isConfig(params[0])) {
|
||||
const { connection, client, ...drizzleConfig } = params[0] as
|
||||
& ({
|
||||
connection?: DrizzleBunSqliteDatabaseConfig | string;
|
||||
client?: TClient;
|
||||
})
|
||||
& DrizzleConfig<TSchema>;
|
||||
|
||||
if (client) return construct(client, drizzleConfig) as any;
|
||||
|
||||
if (typeof connection === 'object') {
|
||||
const { source, ...opts } = connection;
|
||||
|
||||
const options = Object.values(opts).filter((v) => v !== undefined).length ? opts : undefined;
|
||||
|
||||
const instance = new Database(source, options);
|
||||
|
||||
return construct(instance, drizzleConfig) as any;
|
||||
}
|
||||
|
||||
const instance = new Database(connection);
|
||||
|
||||
return construct(instance, drizzleConfig) as any;
|
||||
}
|
||||
|
||||
return construct(params[0] as Database, params[1] as DrizzleConfig<TSchema> | undefined) as any;
|
||||
}
|
||||
|
||||
export namespace drizzle {
|
||||
export function mock<TSchema extends Record<string, unknown> = Record<string, never>>(
|
||||
config?: DrizzleConfig<TSchema>,
|
||||
): BunSQLiteDatabase<TSchema> & {
|
||||
$client: '$client is not available on drizzle.mock()';
|
||||
} {
|
||||
return construct({} as any, config) as any;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './driver.ts';
|
||||
export * from './session.ts';
|
||||
@@ -0,0 +1,11 @@
|
||||
import type { MigrationConfig } from '~/migrator.ts';
|
||||
import { readMigrationFiles } from '~/migrator.ts';
|
||||
import type { BunSQLiteDatabase } from './driver.ts';
|
||||
|
||||
export function migrate<TSchema extends Record<string, unknown>>(
|
||||
db: BunSQLiteDatabase<TSchema>,
|
||||
config: MigrationConfig,
|
||||
) {
|
||||
const migrations = readMigrationFiles(config);
|
||||
db.dialect.migrate(migrations, db.session, config);
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
/// <reference types="bun-types" />
|
||||
|
||||
import type { Database, Statement as BunStatement } from 'bun:sqlite';
|
||||
import { entityKind } from '~/entity.ts';
|
||||
import type { Logger } from '~/logger.ts';
|
||||
import { NoopLogger } from '~/logger.ts';
|
||||
import type { RelationalSchemaConfig, TablesRelationalConfig } from '~/relations.ts';
|
||||
import { fillPlaceholders, type Query, sql } from '~/sql/sql.ts';
|
||||
import type { SQLiteSyncDialect } from '~/sqlite-core/dialect.ts';
|
||||
import { SQLiteTransaction } from '~/sqlite-core/index.ts';
|
||||
import type { SelectedFieldsOrdered } from '~/sqlite-core/query-builders/select.types.ts';
|
||||
import type {
|
||||
PreparedQueryConfig as PreparedQueryConfigBase,
|
||||
SQLiteExecuteMethod,
|
||||
SQLiteTransactionConfig,
|
||||
} from '~/sqlite-core/session.ts';
|
||||
import { SQLitePreparedQuery as PreparedQueryBase, SQLiteSession } from '~/sqlite-core/session.ts';
|
||||
import { mapResultRow } from '~/utils.ts';
|
||||
|
||||
export interface SQLiteBunSessionOptions {
|
||||
logger?: Logger;
|
||||
}
|
||||
|
||||
type PreparedQueryConfig = Omit<PreparedQueryConfigBase, 'statement' | 'run'>;
|
||||
type Statement = BunStatement<any>;
|
||||
|
||||
export class SQLiteBunSession<
|
||||
TFullSchema extends Record<string, unknown>,
|
||||
TSchema extends TablesRelationalConfig,
|
||||
> extends SQLiteSession<'sync', void, TFullSchema, TSchema> {
|
||||
static override readonly [entityKind]: string = 'SQLiteBunSession';
|
||||
|
||||
private logger: Logger;
|
||||
|
||||
constructor(
|
||||
private client: Database,
|
||||
dialect: SQLiteSyncDialect,
|
||||
private schema: RelationalSchemaConfig<TSchema> | undefined,
|
||||
options: SQLiteBunSessionOptions = {},
|
||||
) {
|
||||
super(dialect);
|
||||
this.logger = options.logger ?? new NoopLogger();
|
||||
}
|
||||
|
||||
exec(query: string): void {
|
||||
this.client.exec(query);
|
||||
}
|
||||
|
||||
prepareQuery<T extends Omit<PreparedQueryConfig, 'run'>>(
|
||||
query: Query,
|
||||
fields: SelectedFieldsOrdered | undefined,
|
||||
executeMethod: SQLiteExecuteMethod,
|
||||
isResponseInArrayMode: boolean,
|
||||
customResultMapper?: (rows: unknown[][]) => unknown,
|
||||
): PreparedQuery<T> {
|
||||
const stmt = this.client.prepare(query.sql);
|
||||
return new PreparedQuery(
|
||||
stmt,
|
||||
query,
|
||||
this.logger,
|
||||
fields,
|
||||
executeMethod,
|
||||
isResponseInArrayMode,
|
||||
customResultMapper,
|
||||
);
|
||||
}
|
||||
|
||||
override transaction<T>(
|
||||
transaction: (tx: SQLiteBunTransaction<TFullSchema, TSchema>) => T,
|
||||
config: SQLiteTransactionConfig = {},
|
||||
): T {
|
||||
const tx = new SQLiteBunTransaction('sync', this.dialect, this, this.schema);
|
||||
let result: T | undefined;
|
||||
const nativeTx = this.client.transaction(() => {
|
||||
result = transaction(tx);
|
||||
});
|
||||
nativeTx[config.behavior ?? 'deferred']();
|
||||
return result!;
|
||||
}
|
||||
}
|
||||
|
||||
export class SQLiteBunTransaction<
|
||||
TFullSchema extends Record<string, unknown>,
|
||||
TSchema extends TablesRelationalConfig,
|
||||
> extends SQLiteTransaction<'sync', void, TFullSchema, TSchema> {
|
||||
static override readonly [entityKind]: string = 'SQLiteBunTransaction';
|
||||
|
||||
override transaction<T>(transaction: (tx: SQLiteBunTransaction<TFullSchema, TSchema>) => T): T {
|
||||
const savepointName = `sp${this.nestedIndex}`;
|
||||
const tx = new SQLiteBunTransaction('sync', this.dialect, this.session, this.schema, this.nestedIndex + 1);
|
||||
this.session.run(sql.raw(`savepoint ${savepointName}`));
|
||||
try {
|
||||
const result = transaction(tx);
|
||||
this.session.run(sql.raw(`release savepoint ${savepointName}`));
|
||||
return result;
|
||||
} catch (err) {
|
||||
this.session.run(sql.raw(`rollback to savepoint ${savepointName}`));
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class PreparedQuery<T extends PreparedQueryConfig = PreparedQueryConfig> extends PreparedQueryBase<
|
||||
{ type: 'sync'; run: void; all: T['all']; get: T['get']; values: T['values']; execute: T['execute'] }
|
||||
> {
|
||||
static override readonly [entityKind]: string = 'SQLiteBunPreparedQuery';
|
||||
|
||||
constructor(
|
||||
private stmt: Statement,
|
||||
query: Query,
|
||||
private logger: Logger,
|
||||
private fields: SelectedFieldsOrdered | undefined,
|
||||
executeMethod: SQLiteExecuteMethod,
|
||||
private _isResponseInArrayMode: boolean,
|
||||
private customResultMapper?: (rows: unknown[][]) => unknown,
|
||||
) {
|
||||
super('sync', executeMethod, query);
|
||||
}
|
||||
|
||||
run(placeholderValues?: Record<string, unknown>) {
|
||||
const params = fillPlaceholders(this.query.params, placeholderValues ?? {});
|
||||
this.logger.logQuery(this.query.sql, params);
|
||||
return this.stmt.run(...params);
|
||||
}
|
||||
|
||||
all(placeholderValues?: Record<string, unknown>): T['all'] {
|
||||
const { fields, query, logger, joinsNotNullableMap, stmt, customResultMapper } = this;
|
||||
if (!fields && !customResultMapper) {
|
||||
const params = fillPlaceholders(query.params, placeholderValues ?? {});
|
||||
logger.logQuery(query.sql, params);
|
||||
return stmt.all(...params);
|
||||
}
|
||||
|
||||
const rows = this.values(placeholderValues) as unknown[][];
|
||||
|
||||
if (customResultMapper) {
|
||||
return customResultMapper(rows) as T['all'];
|
||||
}
|
||||
|
||||
return rows.map((row) => mapResultRow(fields!, row, joinsNotNullableMap));
|
||||
}
|
||||
|
||||
get(placeholderValues?: Record<string, unknown>): T['get'] {
|
||||
const params = fillPlaceholders(this.query.params, placeholderValues ?? {});
|
||||
this.logger.logQuery(this.query.sql, params);
|
||||
const row = this.stmt.values(...params)[0];
|
||||
|
||||
if (!row) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const { fields, joinsNotNullableMap, customResultMapper } = this;
|
||||
if (!fields && !customResultMapper) {
|
||||
return row;
|
||||
}
|
||||
|
||||
if (customResultMapper) {
|
||||
return customResultMapper([row]) as T['get'];
|
||||
}
|
||||
|
||||
return mapResultRow(fields!, row, joinsNotNullableMap);
|
||||
}
|
||||
|
||||
values(placeholderValues?: Record<string, unknown>): T['values'] {
|
||||
const params = fillPlaceholders(this.query.params, placeholderValues ?? {});
|
||||
this.logger.logQuery(this.query.sql, params);
|
||||
return this.stmt.values(...params);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
isResponseInArrayMode(): boolean {
|
||||
return this._isResponseInArrayMode;
|
||||
}
|
||||
}
|
||||
Vendored
+78
@@ -0,0 +1,78 @@
|
||||
import { entityKind } from '~/entity.ts';
|
||||
import type { Table } from '~/index.ts';
|
||||
import type { CacheConfig } from './types.ts';
|
||||
|
||||
export abstract class Cache {
|
||||
static readonly [entityKind]: string = 'Cache';
|
||||
|
||||
abstract strategy(): 'explicit' | 'all';
|
||||
|
||||
/**
|
||||
* Invoked if we should check cache for cached response
|
||||
* @param sql
|
||||
* @param tables
|
||||
*/
|
||||
abstract get(
|
||||
key: string,
|
||||
tables: string[],
|
||||
isTag: boolean,
|
||||
isAutoInvalidate?: boolean,
|
||||
): Promise<any[] | undefined>;
|
||||
|
||||
/**
|
||||
* Invoked if new query should be inserted to cache
|
||||
* @param sql
|
||||
* @param tables
|
||||
*/
|
||||
abstract put(
|
||||
hashedQuery: string,
|
||||
response: any,
|
||||
tables: string[],
|
||||
isTag: boolean,
|
||||
config?: CacheConfig,
|
||||
): Promise<void>;
|
||||
|
||||
/**
|
||||
* Invoked if insert, update, delete was invoked
|
||||
* @param tables
|
||||
*/
|
||||
abstract onMutate(
|
||||
params: MutationOption,
|
||||
): Promise<void>;
|
||||
}
|
||||
|
||||
export class NoopCache extends Cache {
|
||||
override strategy() {
|
||||
return 'all' as const;
|
||||
}
|
||||
|
||||
static override readonly [entityKind]: string = 'NoopCache';
|
||||
|
||||
override async get(_key: string): Promise<any[] | undefined> {
|
||||
return undefined;
|
||||
}
|
||||
override async put(
|
||||
_hashedQuery: string,
|
||||
_response: any,
|
||||
_tables: string[],
|
||||
_config?: any,
|
||||
): Promise<void> {
|
||||
// noop
|
||||
}
|
||||
override async onMutate(_params: MutationOption): Promise<void> {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
|
||||
export type MutationOption = { tags?: string | string[]; tables?: Table<any> | Table<any>[] | string | string[] };
|
||||
|
||||
export async function hashQuery(sql: string, params?: any[]) {
|
||||
const dataToHash = `${sql}-${JSON.stringify(params)}`;
|
||||
const encoder = new TextEncoder();
|
||||
const data = encoder.encode(dataToHash);
|
||||
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
|
||||
const hashArray = [...new Uint8Array(hashBuffer)];
|
||||
const hashHex = hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');
|
||||
|
||||
return hashHex;
|
||||
}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
export * from './cache.ts';
|
||||
Vendored
+29
@@ -0,0 +1,29 @@
|
||||
export type CacheConfig = {
|
||||
/**
|
||||
* expire time, in seconds (a positive integer)
|
||||
*/
|
||||
ex?: number;
|
||||
/**
|
||||
* expire time, in milliseconds (a positive integer).
|
||||
*/
|
||||
px?: number;
|
||||
/**
|
||||
* Unix time at which the key will expire, in seconds (a positive integer).
|
||||
*/
|
||||
exat?: number;
|
||||
/**
|
||||
* Unix time at which the key will expire, in milliseconds (a positive integer)
|
||||
*/
|
||||
pxat?: number;
|
||||
/**
|
||||
* Retain the time to live associated with the key.
|
||||
*/
|
||||
keepTtl?: boolean;
|
||||
/**
|
||||
* Set an expiration (TTL or time to live) on one or more fields of a given hash key.
|
||||
* Used for HEXPIRE command
|
||||
*/
|
||||
hexOptions?: 'NX' | 'nx' | 'XX' | 'xx' | 'GT' | 'gt' | 'LT' | 'lt';
|
||||
};
|
||||
|
||||
export type WithCacheConfig = { enable: boolean; config?: CacheConfig; tag?: string; autoInvalidate?: boolean };
|
||||
Vendored
+230
@@ -0,0 +1,230 @@
|
||||
## Caching with Drizzle
|
||||
|
||||
By default, Drizzle does not perform any implicit actions with your queries and mapping. There is no cache under the hood—each query is sent directly to your database, and you can actually see it.
|
||||
|
||||
However, there are cases when you might want to implement a simple caching logic for specific queries or even for all queries. With Drizzle's cache option, you can define how and when the cache is used, how you store and retrieve data, and what actions to take when write statements are executed on the database. It's basically similar to `beforeQuery` hooks, that will be invoked before actual query will be executed. Additionally, Drizzle provides predefined logic for caching. Let's take a look at it.
|
||||
|
||||
To make cache work you would need to define cache callbacks in drizzle instance or use a predefined ones we have in Drizzle, like a `upstashCache()` that was built together with Upstash team
|
||||
|
||||
### Cache overview
|
||||
|
||||
**Using upstash cache with drizzle**
|
||||
|
||||
```ts
|
||||
const db = drizzle(process.env.DB_URL!, { cache: upstashCache() })
|
||||
```
|
||||
|
||||
You can also define custom logic for your cache behavior. This is an example of our NodeKV implementation for the Drizzle cache test suites
|
||||
|
||||
```ts
|
||||
const db = drizzle(process.env.DB_URL!, { cache: new TestGlobalCache() })
|
||||
```
|
||||
|
||||
```ts
|
||||
import Keyv from 'keyv';
|
||||
|
||||
export class TestGlobalCache extends Cache {
|
||||
private globalTtl: number = 1000;
|
||||
// This object will be used to store which query keys were used
|
||||
// for a specific table, so we can later use it for invalidation.
|
||||
private usedTablesPerKey: Record<string, string[]> = {};
|
||||
|
||||
constructor(private kv: Keyv = new Keyv()) {
|
||||
super();
|
||||
}
|
||||
|
||||
// For the strategy, we have two options:
|
||||
// - 'explicit': The cache is used only when .$withCache() is added to a query.
|
||||
// - 'all': All queries are cached globally.
|
||||
// The default behavior is 'explicit'.
|
||||
override strategy(): 'explicit' | 'all' {
|
||||
return 'all';
|
||||
}
|
||||
|
||||
// This function accepts query and parameters that cached into key param,
|
||||
// allowing you to retrieve response values for this query from the cache.
|
||||
override async get(key: string): Promise<any[] | undefined> {
|
||||
const res = await this.kv.get(key) ?? undefined;
|
||||
return res;
|
||||
}
|
||||
|
||||
// This function accepts several options to define how cached data will be stored:
|
||||
// - 'key': A hashed query and parameters.
|
||||
// - 'response': An array of values returned by Drizzle from the database.
|
||||
// - 'tables': An array of tables involved in the select queries. This information is needed for cache invalidation.
|
||||
//
|
||||
// For example, if a query uses the "users" and "posts" tables, you can store this information. Later, when the app executes
|
||||
// any mutation statements on these tables, you can remove the corresponding key from the cache.
|
||||
// If you're okay with eventual consistency for your queries, you can skip this option.
|
||||
override async put(key: string, response: any, tables: string[], config?: CacheConfig): Promise<void> {
|
||||
await this.kv.set(key, response, config ? config.ex : this.globalTtl);
|
||||
for (const table of tables) {
|
||||
const keys = this.usedTablesPerKey[table];
|
||||
if (keys === undefined) {
|
||||
this.usedTablesPerKey[table] = [key];
|
||||
} else {
|
||||
keys.push(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This function is called when insert, update, or delete statements are executed.
|
||||
// You can either skip this step or invalidate queries that used the affected tables.
|
||||
//
|
||||
// The function receives an object with two keys:
|
||||
// - 'tags': Used for queries labeled with a specific tag, allowing you to invalidate by that tag.
|
||||
// - 'tables': The actual tables affected by the insert, update, or delete statements,
|
||||
// helping you track which tables have changed since the last cache update.
|
||||
override async onMutate(params: { tags: string | string[], tables: string | string[] | Table<any> | Table<any>[]}): Promise<void> {
|
||||
const tagsArray = params.tags ? Array.isArray(params.tags) ? params.tags : [params.tags] : [];
|
||||
const tablesArray = params.tables ? Array.isArray(params.tables) ? params.tables : [params.tables] : [];
|
||||
|
||||
const keysToDelete = new Set<string>();
|
||||
|
||||
for (const table of tablesArray) {
|
||||
const tableName = is(table, Table) ? getTableName(table) : table as string;
|
||||
const keys = this.usedTablesPerKey[tableName] ?? [];
|
||||
for (const key of keys) keysToDelete.add(key);
|
||||
}
|
||||
|
||||
if (keysToDelete.size > 0 || tagsArray.length > 0) {
|
||||
for (const tag of tagsArray) {
|
||||
await this.kv.delete(tag);
|
||||
}
|
||||
|
||||
for (const key of keysToDelete) {
|
||||
await this.kv.delete(key);
|
||||
for (const table of tablesArray) {
|
||||
const tableName = is(table, Table) ? getTableName(table) : table as string;
|
||||
this.usedTablesPerKey[tableName] = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Cache definition
|
||||
|
||||
**Define cache credentials, but no cache will be used globally for all queries**
|
||||
|
||||
```ts
|
||||
const db = drizzle(process.env.DB_URL!, { cache: upstashCache({ url: '', token: '' }) })
|
||||
```
|
||||
|
||||
**Define cache credentials, and the cache will be used globally for all queries**
|
||||
|
||||
```ts
|
||||
const db = drizzle(process.env.DB_URL!, { cache: upstashCache({ url: '', token: '', global: true }) })
|
||||
```
|
||||
|
||||
**Define cache credentials with custom config values to be used for all queries, unless overridden**
|
||||
|
||||
```ts
|
||||
const db = drizzle(process.env.DB_URL!, { cache: upstashCache({ url: '', token: '', global: true, config: {} }) })
|
||||
```
|
||||
|
||||
These are all the possible config values that Drizzle supports with the cache layer
|
||||
|
||||
```ts
|
||||
export type CacheConfig = {
|
||||
/**
|
||||
* expire time, in seconds (a positive integer)
|
||||
*/
|
||||
ex?: number;
|
||||
/**
|
||||
* expire time, in milliseconds (a positive integer).
|
||||
*/
|
||||
px?: number;
|
||||
/**
|
||||
* Unix time at which the key will expire, in seconds (a positive integer).
|
||||
*/
|
||||
exat?: number;
|
||||
/**
|
||||
* Unix time at which the key will expire, in milliseconds (a positive integer)
|
||||
*/
|
||||
pxat?: number;
|
||||
/**
|
||||
* Retain the time to live associated with the key.
|
||||
*/
|
||||
keepTtl?: boolean;
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Cache usage
|
||||
|
||||
Once you've provided all the necessary instructions to the Drizzle database instance, you can now use the cache with Drizzle
|
||||
|
||||
**Case 1: Drizzle with global: false option**
|
||||
|
||||
```ts
|
||||
const db = drizzle(process.env.DB_URL!, { cache: upstashCache({ url: '', token: '' }) })
|
||||
```
|
||||
|
||||
In this case, the current query won't use the cache
|
||||
|
||||
```ts
|
||||
const res = await db.select().from(users)
|
||||
|
||||
// However, any mutate operation will trigger the onMutate function in the cache
|
||||
// and attempt to invalidate queries that used the tables involved in this mutation query.
|
||||
await db.insert(users).value({ email: 'cacheman@upstash.com' })
|
||||
```
|
||||
|
||||
If you want the query to actually use the cache, you need to call `.$withCache()`
|
||||
|
||||
```ts
|
||||
const res = await db.select().from(users).$withCache()
|
||||
```
|
||||
|
||||
`.$withCache` has a set of options you can use to manage and config this specific query strategy
|
||||
|
||||
```ts
|
||||
// rewrite the global config options for this specific query
|
||||
.$withCache({ config: {} })
|
||||
|
||||
// give a query custom cache key instead of hashing query+params under the hood
|
||||
.$withCache({ tag: 'custom_key' })
|
||||
|
||||
// disable autoinvalidation for this query, if you are fine with eventual consstnecy for this specific query
|
||||
.$withCache({ autoInvalidate: false })
|
||||
```
|
||||
|
||||
**Case 2: Drizzle with global: true option**
|
||||
|
||||
```ts
|
||||
const db = drizzle(process.env.DB_URL!, { cache: upstashCache({ url: '', token: '', global: true }) })
|
||||
```
|
||||
|
||||
In this case, the current query will use the cache
|
||||
|
||||
```ts
|
||||
const res = await db.select().from(users)
|
||||
```
|
||||
|
||||
If you want the query to disable cache for some specific query, you need to call `.$withCache(false)`
|
||||
|
||||
```ts
|
||||
// cache is disabled for this query
|
||||
const res = await db.select().from(users).$withCache(false)
|
||||
```
|
||||
|
||||
You can also use cache instance from a `db` to force invalidate specific tables or tags you've defined previously
|
||||
|
||||
```ts
|
||||
// Invalidate all queries that use the `users` table. You can do this with the Drizzle instance.
|
||||
await db.$cache?.invalidate({ tables: users });
|
||||
// or
|
||||
await db.$cache?.invalidate({ tables: [users, posts] });
|
||||
|
||||
// Invalidate all queries that use the `usersTable`. You can do this by using just the table name.
|
||||
await db.$cache?.invalidate({ tables: 'usersTable' });
|
||||
// or
|
||||
await db.$cache?.invalidate({ tables: ['usersTable' , 'postsTable' ] });
|
||||
|
||||
// You can also invalidate custom tags defined in any previously executed select queries.
|
||||
await db.$cache?.invalidate({ tags: 'custom_key' });
|
||||
// or
|
||||
await db.$cache?.invalidate({ tags: ['custom_key', 'custom_key1'] });
|
||||
```
|
||||
+213
@@ -0,0 +1,213 @@
|
||||
import { Redis } from '@upstash/redis';
|
||||
import type { MutationOption } from '~/cache/core/index.ts';
|
||||
import { Cache } from '~/cache/core/index.ts';
|
||||
import { entityKind, is } from '~/entity.ts';
|
||||
import { OriginalName, Table } from '~/index.ts';
|
||||
import type { CacheConfig } from '../core/types.ts';
|
||||
|
||||
const getByTagScript = `
|
||||
local tagsMapKey = KEYS[1] -- tags map key
|
||||
local tag = ARGV[1] -- tag
|
||||
|
||||
local compositeTableName = redis.call('HGET', tagsMapKey, tag)
|
||||
if not compositeTableName then
|
||||
return nil
|
||||
end
|
||||
|
||||
local value = redis.call('HGET', compositeTableName, tag)
|
||||
return value
|
||||
`;
|
||||
|
||||
const onMutateScript = `
|
||||
local tagsMapKey = KEYS[1] -- tags map key
|
||||
local tables = {} -- initialize tables array
|
||||
local tags = ARGV -- tags array
|
||||
|
||||
for i = 2, #KEYS do
|
||||
tables[#tables + 1] = KEYS[i] -- add all keys except the first one to tables
|
||||
end
|
||||
|
||||
if #tags > 0 then
|
||||
for _, tag in ipairs(tags) do
|
||||
if tag ~= nil and tag ~= '' then
|
||||
local compositeTableName = redis.call('HGET', tagsMapKey, tag)
|
||||
if compositeTableName then
|
||||
redis.call('HDEL', compositeTableName, tag)
|
||||
end
|
||||
end
|
||||
end
|
||||
redis.call('HDEL', tagsMapKey, unpack(tags))
|
||||
end
|
||||
|
||||
local keysToDelete = {}
|
||||
|
||||
if #tables > 0 then
|
||||
local compositeTableNames = redis.call('SUNION', unpack(tables))
|
||||
for _, compositeTableName in ipairs(compositeTableNames) do
|
||||
keysToDelete[#keysToDelete + 1] = compositeTableName
|
||||
end
|
||||
for _, table in ipairs(tables) do
|
||||
keysToDelete[#keysToDelete + 1] = table
|
||||
end
|
||||
redis.call('DEL', unpack(keysToDelete))
|
||||
end
|
||||
`;
|
||||
|
||||
type Script = ReturnType<Redis['createScript']>;
|
||||
|
||||
type ExpireOptions = 'NX' | 'nx' | 'XX' | 'xx' | 'GT' | 'gt' | 'LT' | 'lt';
|
||||
|
||||
export class UpstashCache extends Cache {
|
||||
static override readonly [entityKind]: string = 'UpstashCache';
|
||||
/**
|
||||
* Prefix for sets which denote the composite table names for each unique table
|
||||
*
|
||||
* Example: In the composite table set of "table1", you may find
|
||||
* `${compositeTablePrefix}table1,table2` and `${compositeTablePrefix}table1,table3`
|
||||
*/
|
||||
private static compositeTableSetPrefix = '__CTS__';
|
||||
/**
|
||||
* Prefix for hashes which map hash or tags to cache values
|
||||
*/
|
||||
private static compositeTablePrefix = '__CT__';
|
||||
/**
|
||||
* Key which holds the mapping of tags to composite table names
|
||||
*
|
||||
* Using this tagsMapKey, you can find the composite table name for a given tag
|
||||
* and get the cache value for that tag:
|
||||
*
|
||||
* ```ts
|
||||
* const compositeTable = redis.hget(tagsMapKey, 'tag1')
|
||||
* console.log(compositeTable) // `${compositeTablePrefix}table1,table2`
|
||||
*
|
||||
* const cachevalue = redis.hget(compositeTable, 'tag1')
|
||||
*/
|
||||
private static tagsMapKey = '__tagsMap__';
|
||||
/**
|
||||
* Queries whose auto invalidation is false aren't stored in their respective
|
||||
* composite table hashes because those hashes are deleted when a mutation
|
||||
* occurs on related tables.
|
||||
*
|
||||
* Instead, they are stored in a separate hash with the prefix
|
||||
* `__nonAutoInvalidate__` to prevent them from being deleted when a mutation
|
||||
*/
|
||||
private static nonAutoInvalidateTablePrefix = '__nonAutoInvalidate__';
|
||||
|
||||
private luaScripts: {
|
||||
getByTagScript: Script;
|
||||
onMutateScript: Script;
|
||||
};
|
||||
|
||||
private internalConfig: { seconds: number; hexOptions?: ExpireOptions };
|
||||
|
||||
constructor(public redis: Redis, config?: CacheConfig, protected useGlobally?: boolean) {
|
||||
super();
|
||||
this.internalConfig = this.toInternalConfig(config);
|
||||
this.luaScripts = {
|
||||
getByTagScript: this.redis.createScript(getByTagScript, { readonly: true }),
|
||||
onMutateScript: this.redis.createScript(onMutateScript),
|
||||
};
|
||||
}
|
||||
|
||||
public strategy() {
|
||||
return this.useGlobally ? 'all' : 'explicit';
|
||||
}
|
||||
|
||||
private toInternalConfig(config?: CacheConfig): { seconds: number; hexOptions?: ExpireOptions } {
|
||||
return config
|
||||
? {
|
||||
seconds: config.ex!,
|
||||
hexOptions: config.hexOptions,
|
||||
}
|
||||
: {
|
||||
seconds: 1,
|
||||
};
|
||||
}
|
||||
|
||||
override async get(
|
||||
key: string,
|
||||
tables: string[],
|
||||
isTag: boolean = false,
|
||||
isAutoInvalidate?: boolean,
|
||||
): Promise<any[] | undefined> {
|
||||
if (!isAutoInvalidate) {
|
||||
const result = await this.redis.hget(UpstashCache.nonAutoInvalidateTablePrefix, key);
|
||||
return result === null ? undefined : result as any[];
|
||||
}
|
||||
|
||||
if (isTag) {
|
||||
const result = await this.luaScripts.getByTagScript.exec([UpstashCache.tagsMapKey], [key]);
|
||||
return result === null ? undefined : result as any[];
|
||||
}
|
||||
|
||||
// Normal cache lookup for the composite key
|
||||
const compositeKey = this.getCompositeKey(tables);
|
||||
const result = await this.redis.hget(compositeKey, key) ?? undefined; // Retrieve result for normal query
|
||||
return result === null ? undefined : result as any[];
|
||||
}
|
||||
|
||||
override async put(
|
||||
key: string,
|
||||
response: any,
|
||||
tables: string[],
|
||||
isTag: boolean = false,
|
||||
config?: CacheConfig,
|
||||
): Promise<void> {
|
||||
const isAutoInvalidate = tables.length !== 0;
|
||||
|
||||
const pipeline = this.redis.pipeline();
|
||||
const ttlSeconds = config && config.ex ? config.ex : this.internalConfig.seconds;
|
||||
const hexOptions = config && config.hexOptions ? config.hexOptions : this.internalConfig?.hexOptions;
|
||||
|
||||
if (!isAutoInvalidate) {
|
||||
if (isTag) {
|
||||
pipeline.hset(UpstashCache.tagsMapKey, { [key]: UpstashCache.nonAutoInvalidateTablePrefix });
|
||||
pipeline.hexpire(UpstashCache.tagsMapKey, key, ttlSeconds, hexOptions);
|
||||
}
|
||||
|
||||
pipeline.hset(UpstashCache.nonAutoInvalidateTablePrefix, { [key]: response });
|
||||
pipeline.hexpire(UpstashCache.nonAutoInvalidateTablePrefix, key, ttlSeconds, hexOptions);
|
||||
await pipeline.exec();
|
||||
return;
|
||||
}
|
||||
|
||||
const compositeKey = this.getCompositeKey(tables);
|
||||
|
||||
pipeline.hset(compositeKey, { [key]: response }); // Store the result with the tag under the composite key
|
||||
pipeline.hexpire(compositeKey, key, ttlSeconds, hexOptions); // Set expiration for the composite key
|
||||
|
||||
if (isTag) {
|
||||
pipeline.hset(UpstashCache.tagsMapKey, { [key]: compositeKey }); // Store the tag and its composite key in the map
|
||||
pipeline.hexpire(UpstashCache.tagsMapKey, key, ttlSeconds, hexOptions); // Set expiration for the tag
|
||||
}
|
||||
|
||||
for (const table of tables) {
|
||||
pipeline.sadd(this.addTablePrefix(table), compositeKey);
|
||||
}
|
||||
|
||||
await pipeline.exec();
|
||||
}
|
||||
|
||||
override async onMutate(params: MutationOption) {
|
||||
const tags = Array.isArray(params.tags) ? params.tags : params.tags ? [params.tags] : [];
|
||||
const tables = Array.isArray(params.tables) ? params.tables : params.tables ? [params.tables] : [];
|
||||
const tableNames: string[] = tables.map((table) => is(table, Table) ? table[OriginalName] : table as string);
|
||||
|
||||
const compositeTableSets = tableNames.map((table) => this.addTablePrefix(table));
|
||||
await this.luaScripts.onMutateScript.exec([UpstashCache.tagsMapKey, ...compositeTableSets], tags);
|
||||
}
|
||||
|
||||
private addTablePrefix = (table: string) => `${UpstashCache.compositeTableSetPrefix}${table}`;
|
||||
private getCompositeKey = (tables: string[]) => `${UpstashCache.compositeTablePrefix}${tables.sort().join(',')}`;
|
||||
}
|
||||
|
||||
export function upstashCache(
|
||||
{ url, token, config, global = false }: { url: string; token: string; config?: CacheConfig; global?: boolean },
|
||||
): UpstashCache {
|
||||
const redis = new Redis({
|
||||
url,
|
||||
token,
|
||||
});
|
||||
|
||||
return new UpstashCache(redis, config, global);
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from './cache.ts';
|
||||
@@ -0,0 +1,76 @@
|
||||
import type { Column } from '~/column.ts';
|
||||
import { entityKind } from './entity.ts';
|
||||
import { Table } from './table.ts';
|
||||
import type { Casing } from './utils.ts';
|
||||
|
||||
export function toSnakeCase(input: string) {
|
||||
const words = input
|
||||
.replace(/['\u2019]/g, '')
|
||||
.match(/[\da-z]+|[A-Z]+(?![a-z])|[A-Z][\da-z]+/g) ?? [];
|
||||
|
||||
return words.map((word) => word.toLowerCase()).join('_');
|
||||
}
|
||||
|
||||
export function toCamelCase(input: string) {
|
||||
const words = input
|
||||
.replace(/['\u2019]/g, '')
|
||||
.match(/[\da-z]+|[A-Z]+(?![a-z])|[A-Z][\da-z]+/g) ?? [];
|
||||
|
||||
return words.reduce((acc, word, i) => {
|
||||
const formattedWord = i === 0 ? word.toLowerCase() : `${word[0]!.toUpperCase()}${word.slice(1)}`;
|
||||
return acc + formattedWord;
|
||||
}, '');
|
||||
}
|
||||
|
||||
function noopCase(input: string) {
|
||||
return input;
|
||||
}
|
||||
|
||||
export class CasingCache {
|
||||
static readonly [entityKind]: string = 'CasingCache';
|
||||
|
||||
/** @internal */
|
||||
cache: Record<string, string> = {};
|
||||
private cachedTables: Record<string, true> = {};
|
||||
private convert: (input: string) => string;
|
||||
|
||||
constructor(casing?: Casing) {
|
||||
this.convert = casing === 'snake_case'
|
||||
? toSnakeCase
|
||||
: casing === 'camelCase'
|
||||
? toCamelCase
|
||||
: noopCase;
|
||||
}
|
||||
|
||||
getColumnCasing(column: Column): string {
|
||||
if (!column.keyAsName) return column.name;
|
||||
|
||||
const schema = column.table[Table.Symbol.Schema] ?? 'public';
|
||||
const tableName = column.table[Table.Symbol.OriginalName];
|
||||
const key = `${schema}.${tableName}.${column.name}`;
|
||||
|
||||
if (!this.cache[key]) {
|
||||
this.cacheTable(column.table);
|
||||
}
|
||||
return this.cache[key]!;
|
||||
}
|
||||
|
||||
private cacheTable(table: Table) {
|
||||
const schema = table[Table.Symbol.Schema] ?? 'public';
|
||||
const tableName = table[Table.Symbol.OriginalName];
|
||||
const tableKey = `${schema}.${tableName}`;
|
||||
|
||||
if (!this.cachedTables[tableKey]) {
|
||||
for (const column of Object.values(table[Table.Symbol.Columns])) {
|
||||
const columnKey = `${tableKey}.${column.name}`;
|
||||
this.cache[columnKey] = this.convert(column.name);
|
||||
}
|
||||
this.cachedTables[tableKey] = true;
|
||||
}
|
||||
}
|
||||
|
||||
clearCache() {
|
||||
this.cache = {};
|
||||
this.cachedTables = {};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,416 @@
|
||||
import { entityKind } from '~/entity.ts';
|
||||
import type { Column } from './column.ts';
|
||||
import type { GelColumn, GelExtraConfigColumn } from './gel-core/index.ts';
|
||||
import type { MySqlColumn } from './mysql-core/index.ts';
|
||||
import type { ExtraConfigColumn, PgColumn, PgSequenceOptions } from './pg-core/index.ts';
|
||||
import type { SingleStoreColumn } from './singlestore-core/index.ts';
|
||||
import type { SQL } from './sql/sql.ts';
|
||||
import type { SQLiteColumn } from './sqlite-core/index.ts';
|
||||
import type { Assume, Simplify } from './utils.ts';
|
||||
|
||||
export type ColumnDataType =
|
||||
| 'string'
|
||||
| 'number'
|
||||
| 'boolean'
|
||||
| 'array'
|
||||
| 'json'
|
||||
| 'date'
|
||||
| 'bigint'
|
||||
| 'custom'
|
||||
| 'buffer'
|
||||
| 'dateDuration'
|
||||
| 'duration'
|
||||
| 'relDuration'
|
||||
| 'localTime'
|
||||
| 'localDate'
|
||||
| 'localDateTime';
|
||||
|
||||
export type Dialect = 'pg' | 'mysql' | 'sqlite' | 'singlestore' | 'common' | 'gel';
|
||||
|
||||
export type GeneratedStorageMode = 'virtual' | 'stored';
|
||||
|
||||
export type GeneratedType = 'always' | 'byDefault';
|
||||
|
||||
export type GeneratedColumnConfig<TDataType> = {
|
||||
as: TDataType | SQL | (() => SQL);
|
||||
type?: GeneratedType;
|
||||
mode?: GeneratedStorageMode;
|
||||
};
|
||||
|
||||
export type GeneratedIdentityConfig = {
|
||||
sequenceName?: string;
|
||||
sequenceOptions?: PgSequenceOptions;
|
||||
type: 'always' | 'byDefault';
|
||||
};
|
||||
|
||||
export interface ColumnBuilderBaseConfig<TDataType extends ColumnDataType, TColumnType extends string> {
|
||||
name: string;
|
||||
dataType: TDataType;
|
||||
columnType: TColumnType;
|
||||
data: unknown;
|
||||
driverParam: unknown;
|
||||
enumValues: string[] | undefined;
|
||||
}
|
||||
|
||||
export type MakeColumnConfig<
|
||||
T extends ColumnBuilderBaseConfig<ColumnDataType, string>,
|
||||
TTableName extends string,
|
||||
TData = T extends { $type: infer U } ? U : T['data'],
|
||||
> = {
|
||||
name: T['name'];
|
||||
tableName: TTableName;
|
||||
dataType: T['dataType'];
|
||||
columnType: T['columnType'];
|
||||
data: TData;
|
||||
driverParam: T['driverParam'];
|
||||
notNull: T extends { notNull: true } ? true : false;
|
||||
hasDefault: T extends { hasDefault: true } ? true : false;
|
||||
isPrimaryKey: T extends { isPrimaryKey: true } ? true : false;
|
||||
isAutoincrement: T extends { isAutoincrement: true } ? true : false;
|
||||
hasRuntimeDefault: T extends { hasRuntimeDefault: true } ? true : false;
|
||||
enumValues: T['enumValues'];
|
||||
baseColumn: T extends { baseBuilder: infer U extends ColumnBuilderBase } ? BuildColumn<TTableName, U, 'common'>
|
||||
: never;
|
||||
identity: T extends { identity: 'always' } ? 'always' : T extends { identity: 'byDefault' } ? 'byDefault' : undefined;
|
||||
generated: T extends { generated: infer G } ? unknown extends G ? undefined
|
||||
: G extends undefined ? undefined
|
||||
: G
|
||||
: undefined;
|
||||
} & {};
|
||||
|
||||
export type ColumnBuilderTypeConfig<
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
T extends ColumnBuilderBaseConfig<ColumnDataType, string>,
|
||||
TTypeConfig extends object = object,
|
||||
> = Simplify<
|
||||
& {
|
||||
brand: 'ColumnBuilder';
|
||||
name: T['name'];
|
||||
dataType: T['dataType'];
|
||||
columnType: T['columnType'];
|
||||
data: T['data'];
|
||||
driverParam: T['driverParam'];
|
||||
notNull: T extends { notNull: infer U } ? U : boolean;
|
||||
hasDefault: T extends { hasDefault: infer U } ? U : boolean;
|
||||
enumValues: T['enumValues'];
|
||||
identity: T extends { identity: infer U } ? U : unknown;
|
||||
generated: T extends { generated: infer G } ? G extends undefined ? unknown : G : unknown;
|
||||
}
|
||||
& TTypeConfig
|
||||
>;
|
||||
|
||||
export type ColumnBuilderRuntimeConfig<TData, TRuntimeConfig extends object = object> = {
|
||||
name: string;
|
||||
keyAsName: boolean;
|
||||
notNull: boolean;
|
||||
default: TData | SQL | undefined;
|
||||
defaultFn: (() => TData | SQL) | undefined;
|
||||
onUpdateFn: (() => TData | SQL) | undefined;
|
||||
hasDefault: boolean;
|
||||
primaryKey: boolean;
|
||||
isUnique: boolean;
|
||||
uniqueName: string | undefined;
|
||||
uniqueType: string | undefined;
|
||||
dataType: string;
|
||||
columnType: string;
|
||||
generated: GeneratedColumnConfig<TData> | undefined;
|
||||
generatedIdentity: GeneratedIdentityConfig | undefined;
|
||||
} & TRuntimeConfig;
|
||||
|
||||
export interface ColumnBuilderExtraConfig {
|
||||
primaryKeyHasDefault?: boolean;
|
||||
}
|
||||
|
||||
export type NotNull<T extends ColumnBuilderBase> = T & {
|
||||
_: {
|
||||
notNull: true;
|
||||
};
|
||||
};
|
||||
|
||||
export type HasDefault<T extends ColumnBuilderBase> = T & {
|
||||
_: {
|
||||
hasDefault: true;
|
||||
};
|
||||
};
|
||||
|
||||
export type IsPrimaryKey<T extends ColumnBuilderBase> = T & {
|
||||
_: {
|
||||
isPrimaryKey: true;
|
||||
};
|
||||
};
|
||||
|
||||
export type IsAutoincrement<T extends ColumnBuilderBase> = T & {
|
||||
_: {
|
||||
isAutoincrement: true;
|
||||
};
|
||||
};
|
||||
|
||||
export type HasRuntimeDefault<T extends ColumnBuilderBase> = T & {
|
||||
_: {
|
||||
hasRuntimeDefault: true;
|
||||
};
|
||||
};
|
||||
|
||||
export type $Type<T extends ColumnBuilderBase, TType> = T & {
|
||||
_: {
|
||||
$type: TType;
|
||||
};
|
||||
};
|
||||
|
||||
export type HasGenerated<T extends ColumnBuilderBase, TGenerated extends {} = {}> = T & {
|
||||
_: {
|
||||
hasDefault: true;
|
||||
generated: TGenerated;
|
||||
};
|
||||
};
|
||||
|
||||
export type IsIdentity<
|
||||
T extends ColumnBuilderBase,
|
||||
TType extends 'always' | 'byDefault',
|
||||
> = T & {
|
||||
_: {
|
||||
notNull: true;
|
||||
hasDefault: true;
|
||||
identity: TType;
|
||||
};
|
||||
};
|
||||
export interface ColumnBuilderBase<
|
||||
T extends ColumnBuilderBaseConfig<ColumnDataType, string> = ColumnBuilderBaseConfig<ColumnDataType, string>,
|
||||
TTypeConfig extends object = object,
|
||||
> {
|
||||
_: ColumnBuilderTypeConfig<T, TTypeConfig>;
|
||||
}
|
||||
|
||||
// To understand how to use `ColumnBuilder` and `AnyColumnBuilder`, see `Column` and `AnyColumn` documentation.
|
||||
export abstract class ColumnBuilder<
|
||||
T extends ColumnBuilderBaseConfig<ColumnDataType, string> = ColumnBuilderBaseConfig<ColumnDataType, string>,
|
||||
TRuntimeConfig extends object = object,
|
||||
TTypeConfig extends object = object,
|
||||
TExtraConfig extends ColumnBuilderExtraConfig = ColumnBuilderExtraConfig,
|
||||
> implements ColumnBuilderBase<T, TTypeConfig> {
|
||||
static readonly [entityKind]: string = 'ColumnBuilder';
|
||||
|
||||
declare _: ColumnBuilderTypeConfig<T, TTypeConfig>;
|
||||
|
||||
protected config: ColumnBuilderRuntimeConfig<T['data'], TRuntimeConfig>;
|
||||
|
||||
constructor(name: T['name'], dataType: T['dataType'], columnType: T['columnType']) {
|
||||
this.config = {
|
||||
name,
|
||||
keyAsName: name === '',
|
||||
notNull: false,
|
||||
default: undefined,
|
||||
hasDefault: false,
|
||||
primaryKey: false,
|
||||
isUnique: false,
|
||||
uniqueName: undefined,
|
||||
uniqueType: undefined,
|
||||
dataType,
|
||||
columnType,
|
||||
generated: undefined,
|
||||
} as ColumnBuilderRuntimeConfig<T['data'], TRuntimeConfig>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the data type of the column. Commonly used with `json` columns. Also, useful for branded types.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const users = pgTable('users', {
|
||||
* id: integer('id').$type<UserId>().primaryKey(),
|
||||
* details: json('details').$type<UserDetails>().notNull(),
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
$type<TType>(): $Type<this, TType> {
|
||||
return this as $Type<this, TType>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a `not null` clause to the column definition.
|
||||
*
|
||||
* Affects the `select` model of the table - columns *without* `not null` will be nullable on select.
|
||||
*/
|
||||
notNull(): NotNull<this> {
|
||||
this.config.notNull = true;
|
||||
return this as NotNull<this>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a `default <value>` clause to the column definition.
|
||||
*
|
||||
* Affects the `insert` model of the table - columns *with* `default` are optional on insert.
|
||||
*
|
||||
* If you need to set a dynamic default value, use {@link $defaultFn} instead.
|
||||
*/
|
||||
default(value: (this['_'] extends { $type: infer U } ? U : this['_']['data']) | SQL): HasDefault<this> {
|
||||
this.config.default = value;
|
||||
this.config.hasDefault = true;
|
||||
return this as HasDefault<this>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a dynamic default value to the column.
|
||||
* The function will be called when the row is inserted, and the returned value will be used as the column value.
|
||||
*
|
||||
* **Note:** This value does not affect the `drizzle-kit` behavior, it is only used at runtime in `drizzle-orm`.
|
||||
*/
|
||||
$defaultFn(
|
||||
fn: () => (this['_'] extends { $type: infer U } ? U : this['_']['data']) | SQL,
|
||||
): HasRuntimeDefault<HasDefault<this>> {
|
||||
this.config.defaultFn = fn;
|
||||
this.config.hasDefault = true;
|
||||
return this as HasRuntimeDefault<HasDefault<this>>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias for {@link $defaultFn}.
|
||||
*/
|
||||
$default = this.$defaultFn;
|
||||
|
||||
/**
|
||||
* Adds a dynamic update value to the column.
|
||||
* The function will be called when the row is updated, and the returned value will be used as the column value if none is provided.
|
||||
* If no `default` (or `$defaultFn`) value is provided, the function will be called when the row is inserted as well, and the returned value will be used as the column value.
|
||||
*
|
||||
* **Note:** This value does not affect the `drizzle-kit` behavior, it is only used at runtime in `drizzle-orm`.
|
||||
*/
|
||||
$onUpdateFn(
|
||||
fn: () => (this['_'] extends { $type: infer U } ? U : this['_']['data']) | SQL,
|
||||
): HasDefault<this> {
|
||||
this.config.onUpdateFn = fn;
|
||||
this.config.hasDefault = true;
|
||||
return this as HasDefault<this>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias for {@link $onUpdateFn}.
|
||||
*/
|
||||
$onUpdate = this.$onUpdateFn;
|
||||
|
||||
/**
|
||||
* Adds a `primary key` clause to the column definition. This implicitly makes the column `not null`.
|
||||
*
|
||||
* In SQLite, `integer primary key` implicitly makes the column auto-incrementing.
|
||||
*/
|
||||
primaryKey(): TExtraConfig['primaryKeyHasDefault'] extends true ? IsPrimaryKey<HasDefault<NotNull<this>>>
|
||||
: IsPrimaryKey<NotNull<this>>
|
||||
{
|
||||
this.config.primaryKey = true;
|
||||
this.config.notNull = true;
|
||||
return this as TExtraConfig['primaryKeyHasDefault'] extends true ? IsPrimaryKey<HasDefault<NotNull<this>>>
|
||||
: IsPrimaryKey<NotNull<this>>;
|
||||
}
|
||||
|
||||
abstract generatedAlwaysAs(
|
||||
as: SQL | T['data'] | (() => SQL),
|
||||
config?: Partial<GeneratedColumnConfig<unknown>>,
|
||||
): HasGenerated<this, {
|
||||
type: 'always';
|
||||
}>;
|
||||
|
||||
/** @internal Sets the name of the column to the key within the table definition if a name was not given. */
|
||||
setName(name: string) {
|
||||
if (this.config.name !== '') return;
|
||||
this.config.name = name;
|
||||
}
|
||||
}
|
||||
|
||||
export type BuildColumn<
|
||||
TTableName extends string,
|
||||
TBuilder extends ColumnBuilderBase,
|
||||
TDialect extends Dialect,
|
||||
> = TDialect extends 'pg' ? PgColumn<
|
||||
MakeColumnConfig<TBuilder['_'], TTableName>,
|
||||
{},
|
||||
Simplify<Omit<TBuilder['_'], keyof MakeColumnConfig<TBuilder['_'], TTableName> | 'brand' | 'dialect'>>
|
||||
>
|
||||
: TDialect extends 'mysql' ? MySqlColumn<
|
||||
MakeColumnConfig<TBuilder['_'], TTableName>,
|
||||
{},
|
||||
Simplify<
|
||||
Omit<
|
||||
TBuilder['_'],
|
||||
| keyof MakeColumnConfig<TBuilder['_'], TTableName>
|
||||
| 'brand'
|
||||
| 'dialect'
|
||||
| 'primaryKeyHasDefault'
|
||||
| 'mysqlColumnBuilderBrand'
|
||||
>
|
||||
>
|
||||
>
|
||||
: TDialect extends 'sqlite' ? SQLiteColumn<
|
||||
MakeColumnConfig<TBuilder['_'], TTableName>,
|
||||
{},
|
||||
Simplify<Omit<TBuilder['_'], keyof MakeColumnConfig<TBuilder['_'], TTableName> | 'brand' | 'dialect'>>
|
||||
>
|
||||
: TDialect extends 'common' ? Column<
|
||||
MakeColumnConfig<TBuilder['_'], TTableName>,
|
||||
{},
|
||||
Simplify<Omit<TBuilder['_'], keyof MakeColumnConfig<TBuilder['_'], TTableName> | 'brand' | 'dialect'>>
|
||||
>
|
||||
: TDialect extends 'singlestore' ? SingleStoreColumn<
|
||||
MakeColumnConfig<TBuilder['_'], TTableName>,
|
||||
{},
|
||||
Simplify<
|
||||
Omit<
|
||||
TBuilder['_'],
|
||||
| keyof MakeColumnConfig<TBuilder['_'], TTableName>
|
||||
| 'brand'
|
||||
| 'dialect'
|
||||
| 'primaryKeyHasDefault'
|
||||
| 'singlestoreColumnBuilderBrand'
|
||||
>
|
||||
>
|
||||
>
|
||||
: TDialect extends 'gel' ? GelColumn<
|
||||
MakeColumnConfig<TBuilder['_'], TTableName>,
|
||||
{},
|
||||
Simplify<Omit<TBuilder['_'], keyof MakeColumnConfig<TBuilder['_'], TTableName> | 'brand' | 'dialect'>>
|
||||
>
|
||||
: never;
|
||||
|
||||
export type BuildIndexColumn<
|
||||
TDialect extends Dialect,
|
||||
> = TDialect extends 'pg' ? ExtraConfigColumn
|
||||
: TDialect extends 'gel' ? GelExtraConfigColumn
|
||||
: never;
|
||||
|
||||
// TODO
|
||||
// try to make sql as well + indexRaw
|
||||
|
||||
// optional after everything will be working as expected
|
||||
// also try to leave only needed methods for extraConfig
|
||||
// make an error if I pass .asc() to fk and so on
|
||||
|
||||
export type BuildColumns<
|
||||
TTableName extends string,
|
||||
TConfigMap extends Record<string, ColumnBuilderBase>,
|
||||
TDialect extends Dialect,
|
||||
> =
|
||||
& {
|
||||
[Key in keyof TConfigMap]: BuildColumn<TTableName, {
|
||||
_:
|
||||
& Omit<TConfigMap[Key]['_'], 'name'>
|
||||
& { name: TConfigMap[Key]['_']['name'] extends '' ? Assume<Key, string> : TConfigMap[Key]['_']['name'] };
|
||||
}, TDialect>;
|
||||
}
|
||||
& {};
|
||||
|
||||
export type BuildExtraConfigColumns<
|
||||
_TTableName extends string,
|
||||
TConfigMap extends Record<string, ColumnBuilderBase>,
|
||||
TDialect extends Dialect,
|
||||
> =
|
||||
& {
|
||||
[Key in keyof TConfigMap]: BuildIndexColumn<TDialect>;
|
||||
}
|
||||
& {};
|
||||
|
||||
export type ChangeColumnTableName<TColumn extends Column, TAlias extends string, TDialect extends Dialect> =
|
||||
TDialect extends 'pg' ? PgColumn<MakeColumnConfig<TColumn['_'], TAlias>>
|
||||
: TDialect extends 'mysql' ? MySqlColumn<MakeColumnConfig<TColumn['_'], TAlias>>
|
||||
: TDialect extends 'singlestore' ? SingleStoreColumn<MakeColumnConfig<TColumn['_'], TAlias>>
|
||||
: TDialect extends 'sqlite' ? SQLiteColumn<MakeColumnConfig<TColumn['_'], TAlias>>
|
||||
: TDialect extends 'gel' ? GelColumn<MakeColumnConfig<TColumn['_'], TAlias>>
|
||||
: never;
|
||||
@@ -0,0 +1,148 @@
|
||||
import type {
|
||||
ColumnBuilderBaseConfig,
|
||||
ColumnBuilderRuntimeConfig,
|
||||
ColumnDataType,
|
||||
GeneratedColumnConfig,
|
||||
GeneratedIdentityConfig,
|
||||
} from './column-builder.ts';
|
||||
import { entityKind } from './entity.ts';
|
||||
import type { DriverValueMapper, SQL, SQLWrapper } from './sql/sql.ts';
|
||||
import type { Table } from './table.ts';
|
||||
import type { Update } from './utils.ts';
|
||||
|
||||
export interface ColumnBaseConfig<
|
||||
TDataType extends ColumnDataType,
|
||||
TColumnType extends string,
|
||||
> extends ColumnBuilderBaseConfig<TDataType, TColumnType> {
|
||||
tableName: string;
|
||||
notNull: boolean;
|
||||
hasDefault: boolean;
|
||||
isPrimaryKey: boolean;
|
||||
isAutoincrement: boolean;
|
||||
hasRuntimeDefault: boolean;
|
||||
}
|
||||
|
||||
export type ColumnTypeConfig<T extends ColumnBaseConfig<ColumnDataType, string>, TTypeConfig extends object> = T & {
|
||||
brand: 'Column';
|
||||
tableName: T['tableName'];
|
||||
name: T['name'];
|
||||
dataType: T['dataType'];
|
||||
columnType: T['columnType'];
|
||||
data: T['data'];
|
||||
driverParam: T['driverParam'];
|
||||
notNull: T['notNull'];
|
||||
hasDefault: T['hasDefault'];
|
||||
isPrimaryKey: T['isPrimaryKey'];
|
||||
isAutoincrement: T['isAutoincrement'];
|
||||
hasRuntimeDefault: T['hasRuntimeDefault'];
|
||||
enumValues: T['enumValues'];
|
||||
baseColumn: T extends { baseColumn: infer U } ? U : unknown;
|
||||
generated: GeneratedColumnConfig<T['data']> | undefined;
|
||||
identity: undefined | 'always' | 'byDefault';
|
||||
} & TTypeConfig;
|
||||
|
||||
export type ColumnRuntimeConfig<TData, TRuntimeConfig extends object> = ColumnBuilderRuntimeConfig<
|
||||
TData,
|
||||
TRuntimeConfig
|
||||
>;
|
||||
|
||||
export interface Column<
|
||||
T extends ColumnBaseConfig<ColumnDataType, string> = ColumnBaseConfig<ColumnDataType, string>,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
TRuntimeConfig extends object = object,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
TTypeConfig extends object = object,
|
||||
> extends DriverValueMapper<T['data'], T['driverParam']>, SQLWrapper {
|
||||
// SQLWrapper runtime implementation is defined in 'sql/sql.ts'
|
||||
}
|
||||
/*
|
||||
`Column` only accepts a full `ColumnConfig` as its generic.
|
||||
To infer parts of the config, use `AnyColumn` that accepts a partial config.
|
||||
See `GetColumnData` for example usage of inferring.
|
||||
*/
|
||||
export abstract class Column<
|
||||
T extends ColumnBaseConfig<ColumnDataType, string> = ColumnBaseConfig<ColumnDataType, string>,
|
||||
TRuntimeConfig extends object = object,
|
||||
TTypeConfig extends object = object,
|
||||
> implements DriverValueMapper<T['data'], T['driverParam']>, SQLWrapper {
|
||||
static readonly [entityKind]: string = 'Column';
|
||||
|
||||
declare readonly _: ColumnTypeConfig<T, TTypeConfig>;
|
||||
|
||||
readonly name: string;
|
||||
readonly keyAsName: boolean;
|
||||
readonly primary: boolean;
|
||||
readonly notNull: boolean;
|
||||
readonly default: T['data'] | SQL | undefined;
|
||||
readonly defaultFn: (() => T['data'] | SQL) | undefined;
|
||||
readonly onUpdateFn: (() => T['data'] | SQL) | undefined;
|
||||
readonly hasDefault: boolean;
|
||||
readonly isUnique: boolean;
|
||||
readonly uniqueName: string | undefined;
|
||||
readonly uniqueType: string | undefined;
|
||||
readonly dataType: T['dataType'];
|
||||
readonly columnType: T['columnType'];
|
||||
readonly enumValues: T['enumValues'] = undefined;
|
||||
readonly generated: GeneratedColumnConfig<T['data']> | undefined = undefined;
|
||||
readonly generatedIdentity: GeneratedIdentityConfig | undefined = undefined;
|
||||
|
||||
protected config: ColumnRuntimeConfig<T['data'], TRuntimeConfig>;
|
||||
|
||||
constructor(
|
||||
readonly table: Table,
|
||||
config: ColumnRuntimeConfig<T['data'], TRuntimeConfig>,
|
||||
) {
|
||||
this.config = config;
|
||||
this.name = config.name;
|
||||
this.keyAsName = config.keyAsName;
|
||||
this.notNull = config.notNull;
|
||||
this.default = config.default;
|
||||
this.defaultFn = config.defaultFn;
|
||||
this.onUpdateFn = config.onUpdateFn;
|
||||
this.hasDefault = config.hasDefault;
|
||||
this.primary = config.primaryKey;
|
||||
this.isUnique = config.isUnique;
|
||||
this.uniqueName = config.uniqueName;
|
||||
this.uniqueType = config.uniqueType;
|
||||
this.dataType = config.dataType as T['dataType'];
|
||||
this.columnType = config.columnType;
|
||||
this.generated = config.generated;
|
||||
this.generatedIdentity = config.generatedIdentity;
|
||||
}
|
||||
|
||||
abstract getSQLType(): string;
|
||||
|
||||
mapFromDriverValue(value: unknown): unknown {
|
||||
return value;
|
||||
}
|
||||
|
||||
mapToDriverValue(value: unknown): unknown {
|
||||
return value;
|
||||
}
|
||||
|
||||
// ** @internal */
|
||||
shouldDisableInsert(): boolean {
|
||||
return this.config.generated !== undefined && this.config.generated.type !== 'byDefault';
|
||||
}
|
||||
}
|
||||
|
||||
export type UpdateColConfig<
|
||||
T extends ColumnBaseConfig<ColumnDataType, string>,
|
||||
TUpdate extends Partial<ColumnBaseConfig<ColumnDataType, string>>,
|
||||
> = Update<T, TUpdate>;
|
||||
|
||||
export type AnyColumn<TPartial extends Partial<ColumnBaseConfig<ColumnDataType, string>> = {}> = Column<
|
||||
Required<Update<ColumnBaseConfig<ColumnDataType, string>, TPartial>>
|
||||
>;
|
||||
|
||||
export type GetColumnData<TColumn extends Column, TInferMode extends 'query' | 'raw' = 'query'> =
|
||||
// dprint-ignore
|
||||
TInferMode extends 'raw' // Raw mode
|
||||
? TColumn['_']['data'] // Just return the underlying type
|
||||
: TColumn['_']['notNull'] extends true // Query mode
|
||||
? TColumn['_']['data'] // Query mode, not null
|
||||
: TColumn['_']['data'] | null; // Query mode, nullable
|
||||
|
||||
export type InferColumnsDataTypes<TColumns extends Record<string, Column>> = {
|
||||
[Key in keyof TColumns]: GetColumnData<TColumns[Key], 'query'>;
|
||||
};
|
||||
@@ -0,0 +1,78 @@
|
||||
/// <reference types="@cloudflare/workers-types" />
|
||||
import type { D1Database as MiniflareD1Database } from '@miniflare/d1';
|
||||
import type { BatchItem, BatchResponse } from '~/batch.ts';
|
||||
import { entityKind } from '~/entity.ts';
|
||||
import { DefaultLogger } from '~/logger.ts';
|
||||
import {
|
||||
createTableRelationsHelpers,
|
||||
extractTablesRelationalConfig,
|
||||
type ExtractTablesWithRelations,
|
||||
type RelationalSchemaConfig,
|
||||
type TablesRelationalConfig,
|
||||
} from '~/relations.ts';
|
||||
import { BaseSQLiteDatabase } from '~/sqlite-core/db.ts';
|
||||
import { SQLiteAsyncDialect } from '~/sqlite-core/dialect.ts';
|
||||
import type { DrizzleConfig, IfNotImported } from '~/utils.ts';
|
||||
import { SQLiteD1Session } from './session.ts';
|
||||
|
||||
export type AnyD1Database = IfNotImported<
|
||||
D1Database,
|
||||
MiniflareD1Database,
|
||||
D1Database | IfNotImported<MiniflareD1Database, never, MiniflareD1Database>
|
||||
>;
|
||||
|
||||
export class DrizzleD1Database<
|
||||
TSchema extends Record<string, unknown> = Record<string, never>,
|
||||
> extends BaseSQLiteDatabase<'async', D1Result, TSchema> {
|
||||
static override readonly [entityKind]: string = 'D1Database';
|
||||
|
||||
/** @internal */
|
||||
declare readonly session: SQLiteD1Session<TSchema, ExtractTablesWithRelations<TSchema>>;
|
||||
|
||||
async batch<U extends BatchItem<'sqlite'>, T extends Readonly<[U, ...U[]]>>(
|
||||
batch: T,
|
||||
): Promise<BatchResponse<T>> {
|
||||
return this.session.batch(batch) as Promise<BatchResponse<T>>;
|
||||
}
|
||||
}
|
||||
|
||||
export function drizzle<
|
||||
TSchema extends Record<string, unknown> = Record<string, never>,
|
||||
TClient extends AnyD1Database = AnyD1Database,
|
||||
>(
|
||||
client: TClient,
|
||||
config: DrizzleConfig<TSchema> = {},
|
||||
): DrizzleD1Database<TSchema> & {
|
||||
$client: TClient;
|
||||
} {
|
||||
const dialect = new SQLiteAsyncDialect({ casing: config.casing });
|
||||
let logger;
|
||||
if (config.logger === true) {
|
||||
logger = new DefaultLogger();
|
||||
} else if (config.logger !== false) {
|
||||
logger = config.logger;
|
||||
}
|
||||
|
||||
let schema: RelationalSchemaConfig<TablesRelationalConfig> | undefined;
|
||||
if (config.schema) {
|
||||
const tablesConfig = extractTablesRelationalConfig(
|
||||
config.schema,
|
||||
createTableRelationsHelpers,
|
||||
);
|
||||
schema = {
|
||||
fullSchema: config.schema,
|
||||
schema: tablesConfig.tables,
|
||||
tableNamesMap: tablesConfig.tableNamesMap,
|
||||
};
|
||||
}
|
||||
|
||||
const session = new SQLiteD1Session(client as D1Database, dialect, schema, { logger, cache: config.cache });
|
||||
const db = new DrizzleD1Database('async', dialect, session, schema) as DrizzleD1Database<TSchema>;
|
||||
(<any> db).$client = client;
|
||||
(<any> db).$cache = config.cache;
|
||||
if ((<any> db).$cache) {
|
||||
(<any> db).$cache['invalidate'] = config.cache?.onMutate;
|
||||
}
|
||||
|
||||
return db as any;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './driver.ts';
|
||||
export * from './session.ts';
|
||||
@@ -0,0 +1,49 @@
|
||||
import type { MigrationConfig } from '~/migrator.ts';
|
||||
import { readMigrationFiles } from '~/migrator.ts';
|
||||
import { sql } from '~/sql/sql.ts';
|
||||
import type { DrizzleD1Database } from './driver.ts';
|
||||
|
||||
export async function migrate<TSchema extends Record<string, unknown>>(
|
||||
db: DrizzleD1Database<TSchema>,
|
||||
config: MigrationConfig,
|
||||
) {
|
||||
const migrations = readMigrationFiles(config);
|
||||
const migrationsTable = config.migrationsTable ?? '__drizzle_migrations';
|
||||
|
||||
const migrationTableCreate = sql`
|
||||
CREATE TABLE IF NOT EXISTS ${sql.identifier(migrationsTable)} (
|
||||
id SERIAL PRIMARY KEY,
|
||||
hash text NOT NULL,
|
||||
created_at numeric
|
||||
)
|
||||
`;
|
||||
await db.session.run(migrationTableCreate);
|
||||
|
||||
const dbMigrations = await db.values<[number, string, string]>(
|
||||
sql`SELECT id, hash, created_at FROM ${sql.identifier(migrationsTable)} ORDER BY created_at DESC LIMIT 1`,
|
||||
);
|
||||
|
||||
const lastDbMigration = dbMigrations[0] ?? undefined;
|
||||
|
||||
const statementToBatch = [];
|
||||
|
||||
for (const migration of migrations) {
|
||||
if (!lastDbMigration || Number(lastDbMigration[2])! < migration.folderMillis) {
|
||||
for (const stmt of migration.sql) {
|
||||
statementToBatch.push(db.run(sql.raw(stmt)));
|
||||
}
|
||||
|
||||
statementToBatch.push(
|
||||
db.run(
|
||||
sql`INSERT INTO ${sql.identifier(migrationsTable)} ("hash", "created_at") VALUES(${
|
||||
sql.raw(`'${migration.hash}'`)
|
||||
}, ${sql.raw(`${migration.folderMillis}`)})`,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (statementToBatch.length > 0) {
|
||||
await db.session.batch(statementToBatch);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,289 @@
|
||||
/// <reference types="@cloudflare/workers-types" />
|
||||
|
||||
import type { BatchItem } from '~/batch.ts';
|
||||
import { type Cache, NoopCache } from '~/cache/core/index.ts';
|
||||
import type { WithCacheConfig } from '~/cache/core/types.ts';
|
||||
import { entityKind } from '~/entity.ts';
|
||||
import type { Logger } from '~/logger.ts';
|
||||
import { NoopLogger } from '~/logger.ts';
|
||||
import type { RelationalSchemaConfig, TablesRelationalConfig } from '~/relations.ts';
|
||||
import type { PreparedQuery } from '~/session.ts';
|
||||
import { fillPlaceholders, type Query, sql } from '~/sql/sql.ts';
|
||||
import type { SQLiteAsyncDialect } from '~/sqlite-core/dialect.ts';
|
||||
import { SQLiteTransaction } from '~/sqlite-core/index.ts';
|
||||
import type { SelectedFieldsOrdered } from '~/sqlite-core/query-builders/select.types.ts';
|
||||
import type {
|
||||
PreparedQueryConfig as PreparedQueryConfigBase,
|
||||
SQLiteExecuteMethod,
|
||||
SQLiteTransactionConfig,
|
||||
} from '~/sqlite-core/session.ts';
|
||||
import { SQLitePreparedQuery, SQLiteSession } from '~/sqlite-core/session.ts';
|
||||
import { mapResultRow } from '~/utils.ts';
|
||||
|
||||
export interface SQLiteD1SessionOptions {
|
||||
logger?: Logger;
|
||||
cache?: Cache;
|
||||
}
|
||||
|
||||
type PreparedQueryConfig = Omit<PreparedQueryConfigBase, 'statement' | 'run'>;
|
||||
|
||||
export class SQLiteD1Session<
|
||||
TFullSchema extends Record<string, unknown>,
|
||||
TSchema extends TablesRelationalConfig,
|
||||
> extends SQLiteSession<'async', D1Result, TFullSchema, TSchema> {
|
||||
static override readonly [entityKind]: string = 'SQLiteD1Session';
|
||||
|
||||
private logger: Logger;
|
||||
private cache: Cache;
|
||||
|
||||
constructor(
|
||||
private client: D1Database,
|
||||
dialect: SQLiteAsyncDialect,
|
||||
private schema: RelationalSchemaConfig<TSchema> | undefined,
|
||||
private options: SQLiteD1SessionOptions = {},
|
||||
) {
|
||||
super(dialect);
|
||||
this.logger = options.logger ?? new NoopLogger();
|
||||
this.cache = options.cache ?? new NoopCache();
|
||||
}
|
||||
|
||||
prepareQuery(
|
||||
query: Query,
|
||||
fields: SelectedFieldsOrdered | undefined,
|
||||
executeMethod: SQLiteExecuteMethod,
|
||||
isResponseInArrayMode: boolean,
|
||||
customResultMapper?: (rows: unknown[][]) => unknown,
|
||||
queryMetadata?: {
|
||||
type: 'select' | 'update' | 'delete' | 'insert';
|
||||
tables: string[];
|
||||
},
|
||||
cacheConfig?: WithCacheConfig,
|
||||
): D1PreparedQuery {
|
||||
const stmt = this.client.prepare(query.sql);
|
||||
return new D1PreparedQuery(
|
||||
stmt,
|
||||
query,
|
||||
this.logger,
|
||||
this.cache,
|
||||
queryMetadata,
|
||||
cacheConfig,
|
||||
fields,
|
||||
executeMethod,
|
||||
isResponseInArrayMode,
|
||||
customResultMapper,
|
||||
);
|
||||
}
|
||||
|
||||
async batch<T extends BatchItem<'sqlite'>[] | readonly BatchItem<'sqlite'>[]>(queries: T) {
|
||||
const preparedQueries: PreparedQuery[] = [];
|
||||
const builtQueries: D1PreparedStatement[] = [];
|
||||
|
||||
for (const query of queries) {
|
||||
const preparedQuery = query._prepare();
|
||||
const builtQuery = preparedQuery.getQuery();
|
||||
preparedQueries.push(preparedQuery);
|
||||
if (builtQuery.params.length > 0) {
|
||||
builtQueries.push((preparedQuery as D1PreparedQuery).stmt.bind(...builtQuery.params));
|
||||
} else {
|
||||
const builtQuery = preparedQuery.getQuery();
|
||||
builtQueries.push(
|
||||
this.client.prepare(builtQuery.sql).bind(...builtQuery.params),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const batchResults = await this.client.batch<any>(builtQueries);
|
||||
return batchResults.map((result, i) => preparedQueries[i]!.mapResult(result, true));
|
||||
}
|
||||
|
||||
override extractRawAllValueFromBatchResult(result: unknown): unknown {
|
||||
return (result as D1Result).results;
|
||||
}
|
||||
|
||||
override extractRawGetValueFromBatchResult(result: unknown): unknown {
|
||||
return (result as D1Result).results[0];
|
||||
}
|
||||
|
||||
override extractRawValuesValueFromBatchResult(result: unknown): unknown {
|
||||
return d1ToRawMapping((result as D1Result).results);
|
||||
}
|
||||
|
||||
override async transaction<T>(
|
||||
transaction: (tx: D1Transaction<TFullSchema, TSchema>) => T | Promise<T>,
|
||||
config?: SQLiteTransactionConfig,
|
||||
): Promise<T> {
|
||||
const tx = new D1Transaction('async', this.dialect, this, this.schema);
|
||||
await this.run(sql.raw(`begin${config?.behavior ? ' ' + config.behavior : ''}`));
|
||||
try {
|
||||
const result = await transaction(tx);
|
||||
await this.run(sql`commit`);
|
||||
return result;
|
||||
} catch (err) {
|
||||
await this.run(sql`rollback`);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class D1Transaction<
|
||||
TFullSchema extends Record<string, unknown>,
|
||||
TSchema extends TablesRelationalConfig,
|
||||
> extends SQLiteTransaction<'async', D1Result, TFullSchema, TSchema> {
|
||||
static override readonly [entityKind]: string = 'D1Transaction';
|
||||
|
||||
override async transaction<T>(transaction: (tx: D1Transaction<TFullSchema, TSchema>) => Promise<T>): Promise<T> {
|
||||
const savepointName = `sp${this.nestedIndex}`;
|
||||
const tx = new D1Transaction('async', this.dialect, this.session, this.schema, this.nestedIndex + 1);
|
||||
await this.session.run(sql.raw(`savepoint ${savepointName}`));
|
||||
try {
|
||||
const result = await transaction(tx);
|
||||
await this.session.run(sql.raw(`release savepoint ${savepointName}`));
|
||||
return result;
|
||||
} catch (err) {
|
||||
await this.session.run(sql.raw(`rollback to savepoint ${savepointName}`));
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function was taken from the D1 implementation: https://github.com/cloudflare/workerd/blob/4aae9f4c7ae30a59a88ca868c4aff88bda85c956/src/cloudflare/internal/d1-api.ts#L287
|
||||
* It may cause issues with duplicated column names in join queries, which should be fixed on the D1 side.
|
||||
* @param results
|
||||
* @returns
|
||||
*/
|
||||
function d1ToRawMapping(results: any) {
|
||||
const rows: unknown[][] = [];
|
||||
for (const row of results) {
|
||||
const entry = Object.keys(row).map((k) => row[k]);
|
||||
rows.push(entry);
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
export class D1PreparedQuery<T extends PreparedQueryConfig = PreparedQueryConfig> extends SQLitePreparedQuery<
|
||||
{ type: 'async'; run: D1Response; all: T['all']; get: T['get']; values: T['values']; execute: T['execute'] }
|
||||
> {
|
||||
static override readonly [entityKind]: string = 'D1PreparedQuery';
|
||||
|
||||
/** @internal */
|
||||
customResultMapper?: (rows: unknown[][], mapColumnValue?: (value: unknown) => unknown) => unknown;
|
||||
|
||||
/** @internal */
|
||||
fields?: SelectedFieldsOrdered;
|
||||
|
||||
/** @internal */
|
||||
stmt: D1PreparedStatement;
|
||||
|
||||
constructor(
|
||||
stmt: D1PreparedStatement,
|
||||
query: Query,
|
||||
private logger: Logger,
|
||||
cache: Cache,
|
||||
queryMetadata: {
|
||||
type: 'select' | 'update' | 'delete' | 'insert';
|
||||
tables: string[];
|
||||
} | undefined,
|
||||
cacheConfig: WithCacheConfig | undefined,
|
||||
fields: SelectedFieldsOrdered | undefined,
|
||||
executeMethod: SQLiteExecuteMethod,
|
||||
private _isResponseInArrayMode: boolean,
|
||||
customResultMapper?: (rows: unknown[][]) => unknown,
|
||||
) {
|
||||
super('async', executeMethod, query, cache, queryMetadata, cacheConfig);
|
||||
this.customResultMapper = customResultMapper;
|
||||
this.fields = fields;
|
||||
this.stmt = stmt;
|
||||
}
|
||||
|
||||
async run(placeholderValues?: Record<string, unknown>): Promise<D1Response> {
|
||||
const params = fillPlaceholders(this.query.params, placeholderValues ?? {});
|
||||
this.logger.logQuery(this.query.sql, params);
|
||||
return await this.queryWithCache(this.query.sql, params, async () => {
|
||||
return this.stmt.bind(...params).run();
|
||||
});
|
||||
}
|
||||
|
||||
async all(placeholderValues?: Record<string, unknown>): Promise<T['all']> {
|
||||
const { fields, query, logger, stmt, customResultMapper } = this;
|
||||
if (!fields && !customResultMapper) {
|
||||
const params = fillPlaceholders(query.params, placeholderValues ?? {});
|
||||
logger.logQuery(query.sql, params);
|
||||
return await this.queryWithCache(query.sql, params, async () => {
|
||||
return stmt.bind(...params).all().then(({ results }) => this.mapAllResult(results!));
|
||||
});
|
||||
}
|
||||
|
||||
const rows = await this.values(placeholderValues);
|
||||
|
||||
return this.mapAllResult(rows);
|
||||
}
|
||||
|
||||
override mapAllResult(rows: unknown, isFromBatch?: boolean): unknown {
|
||||
if (isFromBatch) {
|
||||
rows = d1ToRawMapping((rows as D1Result).results);
|
||||
}
|
||||
|
||||
if (!this.fields && !this.customResultMapper) {
|
||||
return rows;
|
||||
}
|
||||
|
||||
if (this.customResultMapper) {
|
||||
return this.customResultMapper(rows as unknown[][]);
|
||||
}
|
||||
|
||||
return (rows as unknown[][]).map((row) => mapResultRow(this.fields!, row, this.joinsNotNullableMap));
|
||||
}
|
||||
|
||||
async get(placeholderValues?: Record<string, unknown>): Promise<T['get']> {
|
||||
const { fields, joinsNotNullableMap, query, logger, stmt, customResultMapper } = this;
|
||||
if (!fields && !customResultMapper) {
|
||||
const params = fillPlaceholders(query.params, placeholderValues ?? {});
|
||||
logger.logQuery(query.sql, params);
|
||||
return await this.queryWithCache(query.sql, params, async () => {
|
||||
return stmt.bind(...params).all().then(({ results }) => results![0]);
|
||||
});
|
||||
}
|
||||
|
||||
const rows = await this.values(placeholderValues);
|
||||
|
||||
if (!rows[0]) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (customResultMapper) {
|
||||
return customResultMapper(rows) as T['all'];
|
||||
}
|
||||
|
||||
return mapResultRow(fields!, rows[0], joinsNotNullableMap);
|
||||
}
|
||||
|
||||
override mapGetResult(result: unknown, isFromBatch?: boolean): unknown {
|
||||
if (isFromBatch) {
|
||||
result = d1ToRawMapping((result as D1Result).results)[0];
|
||||
}
|
||||
|
||||
if (!this.fields && !this.customResultMapper) {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (this.customResultMapper) {
|
||||
return this.customResultMapper([result as unknown[]]) as T['all'];
|
||||
}
|
||||
|
||||
return mapResultRow(this.fields!, result as unknown[], this.joinsNotNullableMap);
|
||||
}
|
||||
|
||||
async values<T extends any[] = unknown[]>(placeholderValues?: Record<string, unknown>): Promise<T[]> {
|
||||
const params = fillPlaceholders(this.query.params, placeholderValues ?? {});
|
||||
this.logger.logQuery(this.query.sql, params);
|
||||
return await this.queryWithCache(this.query.sql, params, async () => {
|
||||
return this.stmt.bind(...params).raw();
|
||||
});
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
isResponseInArrayMode(): boolean {
|
||||
return this._isResponseInArrayMode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/// <reference types="@cloudflare/workers-types" />
|
||||
import { entityKind } from '~/entity.ts';
|
||||
import { DefaultLogger } from '~/logger.ts';
|
||||
import {
|
||||
createTableRelationsHelpers,
|
||||
extractTablesRelationalConfig,
|
||||
type ExtractTablesWithRelations,
|
||||
type RelationalSchemaConfig,
|
||||
type TablesRelationalConfig,
|
||||
} from '~/relations.ts';
|
||||
import { BaseSQLiteDatabase } from '~/sqlite-core/db.ts';
|
||||
import { SQLiteSyncDialect } from '~/sqlite-core/dialect.ts';
|
||||
import type { DrizzleConfig } from '~/utils.ts';
|
||||
import { SQLiteDOSession } from './session.ts';
|
||||
|
||||
export class DrizzleSqliteDODatabase<
|
||||
TSchema extends Record<string, unknown> = Record<string, never>,
|
||||
> extends BaseSQLiteDatabase<'sync', SqlStorageCursor<Record<string, SqlStorageValue>>, TSchema> {
|
||||
static override readonly [entityKind]: string = 'DrizzleSqliteDODatabase';
|
||||
|
||||
/** @internal */
|
||||
declare readonly session: SQLiteDOSession<TSchema, ExtractTablesWithRelations<TSchema>>;
|
||||
}
|
||||
|
||||
export function drizzle<
|
||||
TSchema extends Record<string, unknown> = Record<string, never>,
|
||||
TClient extends DurableObjectStorage = DurableObjectStorage,
|
||||
>(
|
||||
client: TClient,
|
||||
config: DrizzleConfig<TSchema> = {},
|
||||
): DrizzleSqliteDODatabase<TSchema> & {
|
||||
$client: TClient;
|
||||
} {
|
||||
const dialect = new SQLiteSyncDialect({ casing: config.casing });
|
||||
let logger;
|
||||
if (config.logger === true) {
|
||||
logger = new DefaultLogger();
|
||||
} else if (config.logger !== false) {
|
||||
logger = config.logger;
|
||||
}
|
||||
|
||||
let schema: RelationalSchemaConfig<TablesRelationalConfig> | undefined;
|
||||
if (config.schema) {
|
||||
const tablesConfig = extractTablesRelationalConfig(
|
||||
config.schema,
|
||||
createTableRelationsHelpers,
|
||||
);
|
||||
schema = {
|
||||
fullSchema: config.schema,
|
||||
schema: tablesConfig.tables,
|
||||
tableNamesMap: tablesConfig.tableNamesMap,
|
||||
};
|
||||
}
|
||||
|
||||
const session = new SQLiteDOSession(client as DurableObjectStorage, dialect, schema, { logger });
|
||||
const db = new DrizzleSqliteDODatabase('sync', dialect, session, schema) as DrizzleSqliteDODatabase<TSchema>;
|
||||
(<any> db).$client = client;
|
||||
|
||||
return db as any;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './driver.ts';
|
||||
export * from './session.ts';
|
||||
@@ -0,0 +1,85 @@
|
||||
import type { MigrationMeta } from '~/migrator.ts';
|
||||
import { sql } from '~/sql/index.ts';
|
||||
import type { DrizzleSqliteDODatabase } from './driver.ts';
|
||||
|
||||
interface MigrationConfig {
|
||||
journal: {
|
||||
entries: { idx: number; when: number; tag: string; breakpoints: boolean }[];
|
||||
};
|
||||
migrations: Record<string, string>;
|
||||
}
|
||||
|
||||
function readMigrationFiles({ journal, migrations }: MigrationConfig): MigrationMeta[] {
|
||||
const migrationQueries: MigrationMeta[] = [];
|
||||
|
||||
for (const journalEntry of journal.entries) {
|
||||
const query = migrations[`m${journalEntry.idx.toString().padStart(4, '0')}`];
|
||||
|
||||
if (!query) {
|
||||
throw new Error(`Missing migration: ${journalEntry.tag}`);
|
||||
}
|
||||
|
||||
try {
|
||||
const result = query.split('--> statement-breakpoint').map((it) => {
|
||||
return it;
|
||||
});
|
||||
|
||||
migrationQueries.push({
|
||||
sql: result,
|
||||
bps: journalEntry.breakpoints,
|
||||
folderMillis: journalEntry.when,
|
||||
hash: '',
|
||||
});
|
||||
} catch {
|
||||
throw new Error(`Failed to parse migration: ${journalEntry.tag}`);
|
||||
}
|
||||
}
|
||||
|
||||
return migrationQueries;
|
||||
}
|
||||
|
||||
export async function migrate<
|
||||
TSchema extends Record<string, unknown>,
|
||||
>(
|
||||
db: DrizzleSqliteDODatabase<TSchema>,
|
||||
config: MigrationConfig,
|
||||
): Promise<void> {
|
||||
const migrations = readMigrationFiles(config);
|
||||
|
||||
db.transaction((tx) => {
|
||||
try {
|
||||
const migrationsTable = '__drizzle_migrations';
|
||||
|
||||
const migrationTableCreate = sql`
|
||||
CREATE TABLE IF NOT EXISTS ${sql.identifier(migrationsTable)} (
|
||||
id SERIAL PRIMARY KEY,
|
||||
hash text NOT NULL,
|
||||
created_at numeric
|
||||
)
|
||||
`;
|
||||
db.run(migrationTableCreate);
|
||||
|
||||
const dbMigrations = db.values<[number, string, string]>(
|
||||
sql`SELECT id, hash, created_at FROM ${sql.identifier(migrationsTable)} ORDER BY created_at DESC LIMIT 1`,
|
||||
);
|
||||
|
||||
const lastDbMigration = dbMigrations[0] ?? undefined;
|
||||
|
||||
for (const migration of migrations) {
|
||||
if (!lastDbMigration || Number(lastDbMigration[2])! < migration.folderMillis) {
|
||||
for (const stmt of migration.sql) {
|
||||
db.run(sql.raw(stmt));
|
||||
}
|
||||
db.run(
|
||||
sql`INSERT INTO ${
|
||||
sql.identifier(migrationsTable)
|
||||
} ("hash", "created_at") VALUES(${migration.hash}, ${migration.folderMillis})`,
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
tx.rollback();
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
import { entityKind } from '~/entity.ts';
|
||||
import type { Logger } from '~/logger.ts';
|
||||
import { NoopLogger } from '~/logger.ts';
|
||||
import type { RelationalSchemaConfig, TablesRelationalConfig } from '~/relations.ts';
|
||||
import { fillPlaceholders, type Query } from '~/sql/sql.ts';
|
||||
import { type SQLiteSyncDialect, SQLiteTransaction } from '~/sqlite-core/index.ts';
|
||||
import type { SelectedFieldsOrdered } from '~/sqlite-core/query-builders/select.types.ts';
|
||||
import {
|
||||
type PreparedQueryConfig as PreparedQueryConfigBase,
|
||||
type SQLiteExecuteMethod,
|
||||
SQLiteSession,
|
||||
type SQLiteTransactionConfig,
|
||||
} from '~/sqlite-core/session.ts';
|
||||
import { SQLitePreparedQuery as PreparedQueryBase } from '~/sqlite-core/session.ts';
|
||||
import { mapResultRow } from '~/utils.ts';
|
||||
|
||||
export interface SQLiteDOSessionOptions {
|
||||
logger?: Logger;
|
||||
}
|
||||
|
||||
type PreparedQueryConfig = Omit<PreparedQueryConfigBase, 'statement' | 'run'>;
|
||||
|
||||
export class SQLiteDOSession<TFullSchema extends Record<string, unknown>, TSchema extends TablesRelationalConfig>
|
||||
extends SQLiteSession<
|
||||
'sync',
|
||||
SqlStorageCursor<Record<string, SqlStorageValue>>,
|
||||
TFullSchema,
|
||||
TSchema
|
||||
>
|
||||
{
|
||||
static override readonly [entityKind]: string = 'SQLiteDOSession';
|
||||
|
||||
private logger: Logger;
|
||||
|
||||
constructor(
|
||||
private client: DurableObjectStorage,
|
||||
dialect: SQLiteSyncDialect,
|
||||
private schema: RelationalSchemaConfig<TSchema> | undefined,
|
||||
options: SQLiteDOSessionOptions = {},
|
||||
) {
|
||||
super(dialect);
|
||||
this.logger = options.logger ?? new NoopLogger();
|
||||
}
|
||||
|
||||
prepareQuery<T extends Omit<PreparedQueryConfig, 'run'>>(
|
||||
query: Query,
|
||||
fields: SelectedFieldsOrdered | undefined,
|
||||
executeMethod: SQLiteExecuteMethod,
|
||||
isResponseInArrayMode: boolean,
|
||||
customResultMapper?: (rows: unknown[][]) => unknown,
|
||||
): SQLiteDOPreparedQuery<T> {
|
||||
return new SQLiteDOPreparedQuery(
|
||||
this.client,
|
||||
query,
|
||||
this.logger,
|
||||
fields,
|
||||
executeMethod,
|
||||
isResponseInArrayMode,
|
||||
customResultMapper,
|
||||
);
|
||||
}
|
||||
|
||||
override transaction<T>(
|
||||
transaction: (
|
||||
tx: SQLiteTransaction<'sync', SqlStorageCursor<Record<string, SqlStorageValue>>, TFullSchema, TSchema>,
|
||||
) => T,
|
||||
_config?: SQLiteTransactionConfig,
|
||||
): T {
|
||||
const tx = new SQLiteDOTransaction('sync', this.dialect, this, this.schema);
|
||||
return this.client.transactionSync(() => transaction(tx));
|
||||
}
|
||||
}
|
||||
|
||||
export class SQLiteDOTransaction<TFullSchema extends Record<string, unknown>, TSchema extends TablesRelationalConfig>
|
||||
extends SQLiteTransaction<
|
||||
'sync',
|
||||
SqlStorageCursor<Record<string, SqlStorageValue>>,
|
||||
TFullSchema,
|
||||
TSchema
|
||||
>
|
||||
{
|
||||
static override readonly [entityKind]: string = 'SQLiteDOTransaction';
|
||||
|
||||
override transaction<T>(transaction: (tx: SQLiteDOTransaction<TFullSchema, TSchema>) => T): T {
|
||||
const tx = new SQLiteDOTransaction('sync', this.dialect, this.session, this.schema, this.nestedIndex + 1);
|
||||
return this.session.transaction(() => transaction(tx));
|
||||
}
|
||||
}
|
||||
|
||||
export class SQLiteDOPreparedQuery<T extends PreparedQueryConfig = PreparedQueryConfig> extends PreparedQueryBase<{
|
||||
type: 'sync';
|
||||
run: void;
|
||||
all: T['all'];
|
||||
get: T['get'];
|
||||
values: T['values'];
|
||||
execute: T['execute'];
|
||||
}> {
|
||||
static override readonly [entityKind]: string = 'SQLiteDOPreparedQuery';
|
||||
|
||||
constructor(
|
||||
private client: DurableObjectStorage,
|
||||
query: Query,
|
||||
private logger: Logger,
|
||||
private fields: SelectedFieldsOrdered | undefined,
|
||||
executeMethod: SQLiteExecuteMethod,
|
||||
private _isResponseInArrayMode: boolean,
|
||||
private customResultMapper?: (rows: unknown[][]) => unknown,
|
||||
) {
|
||||
// 3-6 params are for cache. As long as we don't support sync cache - it will be skipped here
|
||||
super('sync', executeMethod, query, undefined, undefined, undefined);
|
||||
}
|
||||
|
||||
run(placeholderValues?: Record<string, unknown>): void {
|
||||
const params = fillPlaceholders(this.query.params, placeholderValues ?? {});
|
||||
this.logger.logQuery(this.query.sql, params);
|
||||
|
||||
params.length > 0 ? this.client.sql.exec(this.query.sql, ...params) : this.client.sql.exec(this.query.sql);
|
||||
}
|
||||
|
||||
all(placeholderValues?: Record<string, unknown>): T['all'] {
|
||||
const { fields, joinsNotNullableMap, query, logger, client, customResultMapper } = this;
|
||||
if (!fields && !customResultMapper) {
|
||||
const params = fillPlaceholders(query.params, placeholderValues ?? {});
|
||||
logger.logQuery(query.sql, params);
|
||||
|
||||
return params.length > 0 ? client.sql.exec(query.sql, ...params).toArray() : client.sql.exec(query.sql).toArray();
|
||||
}
|
||||
|
||||
const rows = this.values(placeholderValues) as unknown[][];
|
||||
|
||||
if (customResultMapper) {
|
||||
return customResultMapper(rows) as T['all'];
|
||||
}
|
||||
|
||||
return rows.map((row) => mapResultRow(fields!, row, joinsNotNullableMap));
|
||||
}
|
||||
|
||||
get(placeholderValues?: Record<string, unknown>): T['get'] {
|
||||
const params = fillPlaceholders(this.query.params, placeholderValues ?? {});
|
||||
this.logger.logQuery(this.query.sql, params);
|
||||
|
||||
const { fields, client, joinsNotNullableMap, customResultMapper, query } = this;
|
||||
if (!fields && !customResultMapper) {
|
||||
return (params.length > 0 ? client.sql.exec(query.sql, ...params) : client.sql.exec(query.sql)).next().value;
|
||||
}
|
||||
|
||||
const rows = this.values(placeholderValues) as unknown[][];
|
||||
const row = rows[0];
|
||||
|
||||
if (!row) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (customResultMapper) {
|
||||
return customResultMapper(rows) as T['get'];
|
||||
}
|
||||
|
||||
return mapResultRow(fields!, row, joinsNotNullableMap);
|
||||
}
|
||||
|
||||
values(placeholderValues?: Record<string, unknown>): T['values'] {
|
||||
const params = fillPlaceholders(this.query.params, placeholderValues ?? {});
|
||||
this.logger.logQuery(this.query.sql, params);
|
||||
|
||||
const res = params.length > 0
|
||||
? this.client.sql.exec(this.query.sql, ...params)
|
||||
: this.client.sql.exec(this.query.sql);
|
||||
|
||||
// @ts-ignore .raw().toArray() exists
|
||||
return res.raw().toArray();
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
isResponseInArrayMode(): boolean {
|
||||
return this._isResponseInArrayMode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
export const entityKind = Symbol.for('drizzle:entityKind');
|
||||
export const hasOwnEntityKind = Symbol.for('drizzle:hasOwnEntityKind');
|
||||
|
||||
export interface DrizzleEntity {
|
||||
[entityKind]: string;
|
||||
}
|
||||
|
||||
export type DrizzleEntityClass<T> =
|
||||
& ((abstract new(...args: any[]) => T) | (new(...args: any[]) => T))
|
||||
& DrizzleEntity;
|
||||
|
||||
export function is<T extends DrizzleEntityClass<any>>(value: any, type: T): value is InstanceType<T> {
|
||||
if (!value || typeof value !== 'object') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (value instanceof type) { // eslint-disable-line no-instanceof/no-instanceof
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!Object.prototype.hasOwnProperty.call(type, entityKind)) {
|
||||
throw new Error(
|
||||
`Class "${
|
||||
type.name ?? '<unknown>'
|
||||
}" doesn't look like a Drizzle entity. If this is incorrect and the class is provided by Drizzle, please report this as a bug.`,
|
||||
);
|
||||
}
|
||||
|
||||
let cls = Object.getPrototypeOf(value).constructor;
|
||||
if (cls) {
|
||||
// Traverse the prototype chain to find the entityKind
|
||||
while (cls) {
|
||||
if (entityKind in cls && cls[entityKind] === type[entityKind]) {
|
||||
return true;
|
||||
}
|
||||
|
||||
cls = Object.getPrototypeOf(cls);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { entityKind } from '~/entity.ts';
|
||||
|
||||
export class DrizzleError extends Error {
|
||||
static readonly [entityKind]: string = 'DrizzleError';
|
||||
|
||||
constructor({ message, cause }: { message?: string; cause?: unknown }) {
|
||||
super(message);
|
||||
this.name = 'DrizzleError';
|
||||
this.cause = cause;
|
||||
}
|
||||
}
|
||||
|
||||
export class DrizzleQueryError extends Error {
|
||||
constructor(
|
||||
public query: string,
|
||||
public params: any[],
|
||||
public override cause?: Error,
|
||||
) {
|
||||
super(`Failed query: ${query}\nparams: ${params}`);
|
||||
Error.captureStackTrace(this, DrizzleQueryError);
|
||||
|
||||
// ES2022+: preserves original error on `.cause`
|
||||
if (cause) (this as any).cause = cause;
|
||||
}
|
||||
}
|
||||
|
||||
export class TransactionRollbackError extends DrizzleError {
|
||||
static override readonly [entityKind]: string = 'TransactionRollbackError';
|
||||
|
||||
constructor() {
|
||||
super({ message: 'Rollback' });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import type { SQLiteDatabase, SQLiteRunResult } from 'expo-sqlite';
|
||||
import { entityKind } from '~/entity.ts';
|
||||
import { DefaultLogger } from '~/logger.ts';
|
||||
import {
|
||||
createTableRelationsHelpers,
|
||||
extractTablesRelationalConfig,
|
||||
type RelationalSchemaConfig,
|
||||
type TablesRelationalConfig,
|
||||
} from '~/relations.ts';
|
||||
import { BaseSQLiteDatabase } from '~/sqlite-core/db.ts';
|
||||
import { SQLiteSyncDialect } from '~/sqlite-core/dialect.ts';
|
||||
import type { DrizzleConfig } from '~/utils.ts';
|
||||
import { ExpoSQLiteSession } from './session.ts';
|
||||
|
||||
export class ExpoSQLiteDatabase<TSchema extends Record<string, unknown> = Record<string, never>>
|
||||
extends BaseSQLiteDatabase<'sync', SQLiteRunResult, TSchema>
|
||||
{
|
||||
static override readonly [entityKind]: string = 'ExpoSQLiteDatabase';
|
||||
}
|
||||
|
||||
export function drizzle<TSchema extends Record<string, unknown> = Record<string, never>>(
|
||||
client: SQLiteDatabase,
|
||||
config: DrizzleConfig<TSchema> = {},
|
||||
): ExpoSQLiteDatabase<TSchema> & {
|
||||
$client: SQLiteDatabase;
|
||||
} {
|
||||
const dialect = new SQLiteSyncDialect({ casing: config.casing });
|
||||
let logger;
|
||||
if (config.logger === true) {
|
||||
logger = new DefaultLogger();
|
||||
} else if (config.logger !== false) {
|
||||
logger = config.logger;
|
||||
}
|
||||
|
||||
let schema: RelationalSchemaConfig<TablesRelationalConfig> | undefined;
|
||||
if (config.schema) {
|
||||
const tablesConfig = extractTablesRelationalConfig(
|
||||
config.schema,
|
||||
createTableRelationsHelpers,
|
||||
);
|
||||
schema = {
|
||||
fullSchema: config.schema,
|
||||
schema: tablesConfig.tables,
|
||||
tableNamesMap: tablesConfig.tableNamesMap,
|
||||
};
|
||||
}
|
||||
|
||||
const session = new ExpoSQLiteSession(client, dialect, schema, { logger });
|
||||
const db = new ExpoSQLiteDatabase('sync', dialect, session, schema) as ExpoSQLiteDatabase<TSchema>;
|
||||
(<any> db).$client = client;
|
||||
|
||||
return db as any;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './driver.ts';
|
||||
export * from './query.ts';
|
||||
export * from './session.ts';
|
||||
@@ -0,0 +1,99 @@
|
||||
import { useEffect, useReducer } from 'react';
|
||||
import type { MigrationMeta } from '~/migrator.ts';
|
||||
import type { ExpoSQLiteDatabase } from './driver.ts';
|
||||
|
||||
interface MigrationConfig {
|
||||
journal: {
|
||||
entries: { idx: number; when: number; tag: string; breakpoints: boolean }[];
|
||||
};
|
||||
migrations: Record<string, string>;
|
||||
}
|
||||
|
||||
async function readMigrationFiles({ journal, migrations }: MigrationConfig): Promise<MigrationMeta[]> {
|
||||
const migrationQueries: MigrationMeta[] = [];
|
||||
|
||||
for await (const journalEntry of journal.entries) {
|
||||
const query = migrations[`m${journalEntry.idx.toString().padStart(4, '0')}`];
|
||||
|
||||
if (!query) {
|
||||
throw new Error(`Missing migration: ${journalEntry.tag}`);
|
||||
}
|
||||
|
||||
try {
|
||||
const result = query.split('--> statement-breakpoint').map((it) => {
|
||||
return it;
|
||||
});
|
||||
|
||||
migrationQueries.push({
|
||||
sql: result,
|
||||
bps: journalEntry.breakpoints,
|
||||
folderMillis: journalEntry.when,
|
||||
hash: '',
|
||||
});
|
||||
} catch {
|
||||
throw new Error(`Failed to parse migration: ${journalEntry.tag}`);
|
||||
}
|
||||
}
|
||||
|
||||
return migrationQueries;
|
||||
}
|
||||
|
||||
export async function migrate<TSchema extends Record<string, unknown>>(
|
||||
db: ExpoSQLiteDatabase<TSchema>,
|
||||
config: MigrationConfig,
|
||||
) {
|
||||
const migrations = await readMigrationFiles(config);
|
||||
return db.dialect.migrate(migrations, db.session);
|
||||
}
|
||||
|
||||
interface State {
|
||||
success: boolean;
|
||||
error?: Error;
|
||||
}
|
||||
|
||||
type Action =
|
||||
| { type: 'migrating' }
|
||||
| { type: 'migrated'; payload: true }
|
||||
| { type: 'error'; payload: Error };
|
||||
|
||||
export const useMigrations = (db: ExpoSQLiteDatabase<any>, migrations: {
|
||||
journal: {
|
||||
entries: { idx: number; when: number; tag: string; breakpoints: boolean }[];
|
||||
};
|
||||
migrations: Record<string, string>;
|
||||
}): State => {
|
||||
const initialState: State = {
|
||||
success: false,
|
||||
error: undefined,
|
||||
};
|
||||
|
||||
const fetchReducer = (state: State, action: Action): State => {
|
||||
switch (action.type) {
|
||||
case 'migrating': {
|
||||
return { ...initialState };
|
||||
}
|
||||
case 'migrated': {
|
||||
return { ...initialState, success: action.payload };
|
||||
}
|
||||
case 'error': {
|
||||
return { ...initialState, error: action.payload };
|
||||
}
|
||||
default: {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const [state, dispatch] = useReducer(fetchReducer, initialState);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch({ type: 'migrating' });
|
||||
migrate(db, migrations as any).then(() => {
|
||||
dispatch({ type: 'migrated', payload: true });
|
||||
}).catch((error) => {
|
||||
dispatch({ type: 'error', payload: error as Error });
|
||||
});
|
||||
}, []);
|
||||
|
||||
return state;
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
import { addDatabaseChangeListener } from 'expo-sqlite';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { is } from '~/entity.ts';
|
||||
import { SQL } from '~/sql/sql.ts';
|
||||
import type { AnySQLiteSelect } from '~/sqlite-core/index.ts';
|
||||
import { getTableConfig, getViewConfig, SQLiteTable, SQLiteView } from '~/sqlite-core/index.ts';
|
||||
import { SQLiteRelationalQuery } from '~/sqlite-core/query-builders/query.ts';
|
||||
import { Subquery } from '~/subquery.ts';
|
||||
|
||||
export const useLiveQuery = <T extends Pick<AnySQLiteSelect, '_' | 'then'> | SQLiteRelationalQuery<'sync', unknown>>(
|
||||
query: T,
|
||||
deps: unknown[] = [],
|
||||
) => {
|
||||
const [data, setData] = useState<Awaited<T>>(
|
||||
(is(query, SQLiteRelationalQuery) && query.mode === 'first' ? undefined : []) as Awaited<T>,
|
||||
);
|
||||
const [error, setError] = useState<Error>();
|
||||
const [updatedAt, setUpdatedAt] = useState<Date>();
|
||||
|
||||
useEffect(() => {
|
||||
const entity = is(query, SQLiteRelationalQuery) ? query.table : (query as AnySQLiteSelect).config.table;
|
||||
|
||||
if (is(entity, Subquery) || is(entity, SQL)) {
|
||||
setError(new Error('Selecting from subqueries and SQL are not supported in useLiveQuery'));
|
||||
return;
|
||||
}
|
||||
|
||||
let listener: ReturnType<typeof addDatabaseChangeListener> | undefined;
|
||||
|
||||
const handleData = (data: any) => {
|
||||
setData(data);
|
||||
setUpdatedAt(new Date());
|
||||
};
|
||||
|
||||
query.then(handleData).catch(setError);
|
||||
|
||||
if (is(entity, SQLiteTable) || is(entity, SQLiteView)) {
|
||||
const config = is(entity, SQLiteTable) ? getTableConfig(entity) : getViewConfig(entity);
|
||||
listener = addDatabaseChangeListener(({ tableName }) => {
|
||||
if (config.name === tableName) {
|
||||
query.then(handleData).catch(setError);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return () => {
|
||||
listener?.remove();
|
||||
};
|
||||
}, deps);
|
||||
|
||||
return {
|
||||
data,
|
||||
error,
|
||||
updatedAt,
|
||||
} as const;
|
||||
};
|
||||
@@ -0,0 +1,175 @@
|
||||
import type { SQLiteDatabase, SQLiteRunResult, SQLiteStatement } from 'expo-sqlite';
|
||||
import { entityKind } from '~/entity.ts';
|
||||
import type { Logger } from '~/logger.ts';
|
||||
import { NoopLogger } from '~/logger.ts';
|
||||
import type { RelationalSchemaConfig, TablesRelationalConfig } from '~/relations.ts';
|
||||
import { fillPlaceholders, type Query, sql } from '~/sql/sql.ts';
|
||||
import type { SQLiteSyncDialect } from '~/sqlite-core/dialect.ts';
|
||||
import { SQLiteTransaction } from '~/sqlite-core/index.ts';
|
||||
import type { SelectedFieldsOrdered } from '~/sqlite-core/query-builders/select.types.ts';
|
||||
import {
|
||||
type PreparedQueryConfig as PreparedQueryConfigBase,
|
||||
type SQLiteExecuteMethod,
|
||||
SQLitePreparedQuery,
|
||||
SQLiteSession,
|
||||
type SQLiteTransactionConfig,
|
||||
} from '~/sqlite-core/session.ts';
|
||||
import { mapResultRow } from '~/utils.ts';
|
||||
|
||||
export interface ExpoSQLiteSessionOptions {
|
||||
logger?: Logger;
|
||||
}
|
||||
|
||||
type PreparedQueryConfig = Omit<PreparedQueryConfigBase, 'statement' | 'run'>;
|
||||
|
||||
export class ExpoSQLiteSession<
|
||||
TFullSchema extends Record<string, unknown>,
|
||||
TSchema extends TablesRelationalConfig,
|
||||
> extends SQLiteSession<'sync', SQLiteRunResult, TFullSchema, TSchema> {
|
||||
static override readonly [entityKind]: string = 'ExpoSQLiteSession';
|
||||
|
||||
private logger: Logger;
|
||||
|
||||
constructor(
|
||||
private client: SQLiteDatabase,
|
||||
dialect: SQLiteSyncDialect,
|
||||
private schema: RelationalSchemaConfig<TSchema> | undefined,
|
||||
options: ExpoSQLiteSessionOptions = {},
|
||||
) {
|
||||
super(dialect);
|
||||
this.logger = options.logger ?? new NoopLogger();
|
||||
}
|
||||
|
||||
prepareQuery<T extends Omit<PreparedQueryConfig, 'run'>>(
|
||||
query: Query,
|
||||
fields: SelectedFieldsOrdered | undefined,
|
||||
executeMethod: SQLiteExecuteMethod,
|
||||
isResponseInArrayMode: boolean,
|
||||
customResultMapper?: (rows: unknown[][]) => unknown,
|
||||
): ExpoSQLitePreparedQuery<T> {
|
||||
const stmt = this.client.prepareSync(query.sql);
|
||||
return new ExpoSQLitePreparedQuery(
|
||||
stmt,
|
||||
query,
|
||||
this.logger,
|
||||
fields,
|
||||
executeMethod,
|
||||
isResponseInArrayMode,
|
||||
customResultMapper,
|
||||
);
|
||||
}
|
||||
|
||||
override transaction<T>(
|
||||
transaction: (tx: ExpoSQLiteTransaction<TFullSchema, TSchema>) => T,
|
||||
config: SQLiteTransactionConfig = {},
|
||||
): T {
|
||||
const tx = new ExpoSQLiteTransaction('sync', this.dialect, this, this.schema);
|
||||
this.run(sql.raw(`begin${config?.behavior ? ' ' + config.behavior : ''}`));
|
||||
try {
|
||||
const result = transaction(tx);
|
||||
this.run(sql`commit`);
|
||||
return result;
|
||||
} catch (err) {
|
||||
this.run(sql`rollback`);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class ExpoSQLiteTransaction<
|
||||
TFullSchema extends Record<string, unknown>,
|
||||
TSchema extends TablesRelationalConfig,
|
||||
> extends SQLiteTransaction<'sync', SQLiteRunResult, TFullSchema, TSchema> {
|
||||
static override readonly [entityKind]: string = 'ExpoSQLiteTransaction';
|
||||
|
||||
override transaction<T>(transaction: (tx: ExpoSQLiteTransaction<TFullSchema, TSchema>) => T): T {
|
||||
const savepointName = `sp${this.nestedIndex}`;
|
||||
const tx = new ExpoSQLiteTransaction('sync', this.dialect, this.session, this.schema, this.nestedIndex + 1);
|
||||
this.session.run(sql.raw(`savepoint ${savepointName}`));
|
||||
try {
|
||||
const result = transaction(tx);
|
||||
this.session.run(sql.raw(`release savepoint ${savepointName}`));
|
||||
return result;
|
||||
} catch (err) {
|
||||
this.session.run(sql.raw(`rollback to savepoint ${savepointName}`));
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class ExpoSQLitePreparedQuery<T extends PreparedQueryConfig = PreparedQueryConfig> extends SQLitePreparedQuery<
|
||||
{ type: 'sync'; run: SQLiteRunResult; all: T['all']; get: T['get']; values: T['values']; execute: T['execute'] }
|
||||
> {
|
||||
static override readonly [entityKind]: string = 'ExpoSQLitePreparedQuery';
|
||||
|
||||
constructor(
|
||||
private stmt: SQLiteStatement,
|
||||
query: Query,
|
||||
private logger: Logger,
|
||||
private fields: SelectedFieldsOrdered | undefined,
|
||||
executeMethod: SQLiteExecuteMethod,
|
||||
private _isResponseInArrayMode: boolean,
|
||||
private customResultMapper?: (rows: unknown[][]) => unknown,
|
||||
) {
|
||||
super('sync', executeMethod, query);
|
||||
}
|
||||
|
||||
run(placeholderValues?: Record<string, unknown>): SQLiteRunResult {
|
||||
const params = fillPlaceholders(this.query.params, placeholderValues ?? {});
|
||||
this.logger.logQuery(this.query.sql, params);
|
||||
const { changes, lastInsertRowId } = this.stmt.executeSync(params as any[]);
|
||||
return {
|
||||
changes,
|
||||
lastInsertRowId,
|
||||
};
|
||||
}
|
||||
|
||||
all(placeholderValues?: Record<string, unknown>): T['all'] {
|
||||
const { fields, joinsNotNullableMap, query, logger, stmt, customResultMapper } = this;
|
||||
if (!fields && !customResultMapper) {
|
||||
const params = fillPlaceholders(query.params, placeholderValues ?? {});
|
||||
logger.logQuery(query.sql, params);
|
||||
return stmt.executeSync(params as any[]).getAllSync();
|
||||
}
|
||||
|
||||
const rows = this.values(placeholderValues) as unknown[][];
|
||||
if (customResultMapper) {
|
||||
return customResultMapper(rows) as T['all'];
|
||||
}
|
||||
return rows.map((row) => mapResultRow(fields!, row, joinsNotNullableMap));
|
||||
}
|
||||
|
||||
get(placeholderValues?: Record<string, unknown>): T['get'] {
|
||||
const params = fillPlaceholders(this.query.params, placeholderValues ?? {});
|
||||
this.logger.logQuery(this.query.sql, params);
|
||||
|
||||
const { fields, stmt, joinsNotNullableMap, customResultMapper } = this;
|
||||
if (!fields && !customResultMapper) {
|
||||
return stmt.executeSync(params as any[]).getFirstSync();
|
||||
}
|
||||
|
||||
const rows = this.values(placeholderValues) as unknown[][];
|
||||
const row = rows[0];
|
||||
|
||||
if (!row) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (customResultMapper) {
|
||||
return customResultMapper(rows) as T['get'];
|
||||
}
|
||||
|
||||
return mapResultRow(fields!, row, joinsNotNullableMap);
|
||||
}
|
||||
|
||||
values(placeholderValues?: Record<string, unknown>): T['values'] {
|
||||
const params = fillPlaceholders(this.query.params, placeholderValues ?? {});
|
||||
this.logger.logQuery(this.query.sql, params);
|
||||
return this.stmt.executeForRawResultSync(params as any[]).getAllSync();
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
isResponseInArrayMode(): boolean {
|
||||
return this._isResponseInArrayMode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { TableAliasProxyHandler } from '~/alias.ts';
|
||||
import type { BuildAliasTable } from './query-builders/select.types.ts';
|
||||
|
||||
import type { GelTable } from './table.ts';
|
||||
import type { GelViewBase } from './view-base.ts';
|
||||
|
||||
export function alias<TTable extends GelTable | GelViewBase, TAlias extends string>(
|
||||
table: TTable,
|
||||
alias: TAlias,
|
||||
): BuildAliasTable<TTable, TAlias> {
|
||||
return new Proxy(table, new TableAliasProxyHandler(alias, false)) as any;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { entityKind } from '~/entity.ts';
|
||||
import type { SQL } from '~/sql/index.ts';
|
||||
import type { GelTable } from './table.ts';
|
||||
|
||||
export class CheckBuilder {
|
||||
static readonly [entityKind]: string = 'GelCheckBuilder';
|
||||
|
||||
protected brand!: 'GelConstraintBuilder';
|
||||
|
||||
constructor(public name: string, public value: SQL) {}
|
||||
|
||||
/** @internal */
|
||||
build(table: GelTable): Check {
|
||||
return new Check(table, this);
|
||||
}
|
||||
}
|
||||
|
||||
export class Check {
|
||||
static readonly [entityKind]: string = 'GelCheck';
|
||||
|
||||
readonly name: string;
|
||||
readonly value: SQL;
|
||||
|
||||
constructor(public table: GelTable, builder: CheckBuilder) {
|
||||
this.name = builder.name;
|
||||
this.value = builder.value;
|
||||
}
|
||||
}
|
||||
|
||||
export function check(name: string, value: SQL): CheckBuilder {
|
||||
return new CheckBuilder(name, value);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { bigint } from './bigint.ts';
|
||||
import { bigintT } from './bigintT.ts';
|
||||
import { boolean } from './boolean.ts';
|
||||
import { bytes } from './bytes.ts';
|
||||
import { customType } from './custom.ts';
|
||||
import { dateDuration } from './date-duration.ts';
|
||||
import { decimal } from './decimal.ts';
|
||||
import { doublePrecision } from './double-precision.ts';
|
||||
import { duration } from './duration.ts';
|
||||
import { integer } from './integer.ts';
|
||||
import { json } from './json.ts';
|
||||
import { localDate } from './localdate.ts';
|
||||
import { localTime } from './localtime.ts';
|
||||
import { real } from './real.ts';
|
||||
import { relDuration } from './relative-duration.ts';
|
||||
import { smallint } from './smallint.ts';
|
||||
import { text } from './text.ts';
|
||||
import { timestamp } from './timestamp.ts';
|
||||
import { timestamptz } from './timestamptz.ts';
|
||||
import { uuid } from './uuid.ts';
|
||||
|
||||
// TODO add
|
||||
export function getGelColumnBuilders() {
|
||||
return {
|
||||
localDate,
|
||||
localTime,
|
||||
decimal,
|
||||
dateDuration,
|
||||
bigintT,
|
||||
duration,
|
||||
relDuration,
|
||||
bytes,
|
||||
customType,
|
||||
bigint,
|
||||
boolean,
|
||||
doublePrecision,
|
||||
integer,
|
||||
json,
|
||||
real,
|
||||
smallint,
|
||||
text,
|
||||
timestamptz,
|
||||
uuid,
|
||||
timestamp,
|
||||
};
|
||||
}
|
||||
|
||||
export type GelColumnsBuilders = ReturnType<typeof getGelColumnBuilders>;
|
||||
@@ -0,0 +1,46 @@
|
||||
import type { ColumnBuilderBaseConfig, ColumnBuilderRuntimeConfig, MakeColumnConfig } from '~/column-builder.ts';
|
||||
import type { ColumnBaseConfig } from '~/column.ts';
|
||||
import { entityKind } from '~/entity.ts';
|
||||
import type { AnyGelTable } from '~/gel-core/table.ts';
|
||||
import { GelColumn } from './common.ts';
|
||||
import { GelIntColumnBaseBuilder } from './int.common.ts';
|
||||
|
||||
export type GelInt53BuilderInitial<TName extends string> = GelInt53Builder<{
|
||||
name: TName;
|
||||
dataType: 'number';
|
||||
columnType: 'GelInt53';
|
||||
data: number;
|
||||
driverParam: number;
|
||||
enumValues: undefined;
|
||||
}>;
|
||||
|
||||
export class GelInt53Builder<T extends ColumnBuilderBaseConfig<'number', 'GelInt53'>>
|
||||
extends GelIntColumnBaseBuilder<T>
|
||||
{
|
||||
static override readonly [entityKind]: string = 'GelInt53Builder';
|
||||
|
||||
constructor(name: T['name']) {
|
||||
super(name, 'number', 'GelInt53');
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
override build<TTableName extends string>(
|
||||
table: AnyGelTable<{ name: TTableName }>,
|
||||
): GelInt53<MakeColumnConfig<T, TTableName>> {
|
||||
return new GelInt53<MakeColumnConfig<T, TTableName>>(table, this.config as ColumnBuilderRuntimeConfig<any, any>);
|
||||
}
|
||||
}
|
||||
|
||||
export class GelInt53<T extends ColumnBaseConfig<'number', 'GelInt53'>> extends GelColumn<T> {
|
||||
static override readonly [entityKind]: string = 'GelInt53';
|
||||
|
||||
getSQLType(): string {
|
||||
return 'bigint';
|
||||
}
|
||||
}
|
||||
|
||||
export function bigint(): GelInt53BuilderInitial<''>;
|
||||
export function bigint<TName extends string>(name: TName): GelInt53BuilderInitial<TName>;
|
||||
export function bigint(name?: string) {
|
||||
return new GelInt53Builder(name ?? '');
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import type { ColumnBuilderBaseConfig, ColumnBuilderRuntimeConfig, MakeColumnConfig } from '~/column-builder.ts';
|
||||
import type { ColumnBaseConfig } from '~/column.ts';
|
||||
import { entityKind } from '~/entity.ts';
|
||||
import type { AnyGelTable } from '~/gel-core/table.ts';
|
||||
import { GelColumn } from './common.ts';
|
||||
import { GelIntColumnBaseBuilder } from './int.common.ts';
|
||||
|
||||
export type GelBigInt64BuilderInitial<TName extends string> = GelBigInt64Builder<{
|
||||
name: TName;
|
||||
dataType: 'bigint';
|
||||
columnType: 'GelBigInt64';
|
||||
data: bigint;
|
||||
driverParam: bigint;
|
||||
enumValues: undefined;
|
||||
}>;
|
||||
|
||||
export class GelBigInt64Builder<T extends ColumnBuilderBaseConfig<'bigint', 'GelBigInt64'>>
|
||||
extends GelIntColumnBaseBuilder<T>
|
||||
{
|
||||
static override readonly [entityKind]: string = 'GelBigInt64Builder';
|
||||
|
||||
constructor(name: T['name']) {
|
||||
super(name, 'bigint', 'GelBigInt64');
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
override build<TTableName extends string>(
|
||||
table: AnyGelTable<{ name: TTableName }>,
|
||||
): GelBigInt64<MakeColumnConfig<T, TTableName>> {
|
||||
return new GelBigInt64<MakeColumnConfig<T, TTableName>>(
|
||||
table,
|
||||
this.config as ColumnBuilderRuntimeConfig<any, any>,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class GelBigInt64<T extends ColumnBaseConfig<'bigint', 'GelBigInt64'>> extends GelColumn<T> {
|
||||
static override readonly [entityKind]: string = 'GelBigInt64';
|
||||
|
||||
getSQLType(): string {
|
||||
return 'edgedbt.bigint_t';
|
||||
}
|
||||
|
||||
override mapFromDriverValue(value: string): bigint {
|
||||
return BigInt(value as string); // TODO ts error if remove 'as string'
|
||||
}
|
||||
}
|
||||
|
||||
export function bigintT(): GelBigInt64BuilderInitial<''>;
|
||||
export function bigintT<TName extends string>(name: TName): GelBigInt64BuilderInitial<TName>;
|
||||
export function bigintT(name?: string) {
|
||||
return new GelBigInt64Builder(name ?? '');
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { ColumnBuilderBaseConfig, ColumnBuilderRuntimeConfig, MakeColumnConfig } from '~/column-builder.ts';
|
||||
import type { ColumnBaseConfig } from '~/column.ts';
|
||||
import { entityKind } from '~/entity.ts';
|
||||
import type { AnyGelTable } from '~/gel-core/table.ts';
|
||||
import { GelColumn, GelColumnBuilder } from './common.ts';
|
||||
|
||||
export type GelBooleanBuilderInitial<TName extends string> = GelBooleanBuilder<{
|
||||
name: TName;
|
||||
dataType: 'boolean';
|
||||
columnType: 'GelBoolean';
|
||||
data: boolean;
|
||||
driverParam: boolean;
|
||||
enumValues: undefined;
|
||||
}>;
|
||||
|
||||
export class GelBooleanBuilder<T extends ColumnBuilderBaseConfig<'boolean', 'GelBoolean'>> extends GelColumnBuilder<T> {
|
||||
static override readonly [entityKind]: string = 'GelBooleanBuilder';
|
||||
|
||||
constructor(name: T['name']) {
|
||||
super(name, 'boolean', 'GelBoolean');
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
override build<TTableName extends string>(
|
||||
table: AnyGelTable<{ name: TTableName }>,
|
||||
): GelBoolean<MakeColumnConfig<T, TTableName>> {
|
||||
return new GelBoolean<MakeColumnConfig<T, TTableName>>(table, this.config as ColumnBuilderRuntimeConfig<any, any>);
|
||||
}
|
||||
}
|
||||
|
||||
export class GelBoolean<T extends ColumnBaseConfig<'boolean', 'GelBoolean'>> extends GelColumn<T> {
|
||||
static override readonly [entityKind]: string = 'GelBoolean';
|
||||
|
||||
getSQLType(): string {
|
||||
return 'boolean';
|
||||
}
|
||||
}
|
||||
|
||||
export function boolean(): GelBooleanBuilderInitial<''>;
|
||||
export function boolean<TName extends string>(name: TName): GelBooleanBuilderInitial<TName>;
|
||||
export function boolean(name?: string) {
|
||||
return new GelBooleanBuilder(name ?? '');
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import type { ColumnBuilderBaseConfig, ColumnBuilderRuntimeConfig, MakeColumnConfig } from '~/column-builder.ts';
|
||||
import type { ColumnBaseConfig } from '~/column.ts';
|
||||
import { entityKind } from '~/entity.ts';
|
||||
import type { AnyGelTable } from '~/gel-core/table.ts';
|
||||
import { GelColumn, GelColumnBuilder } from './common.ts';
|
||||
|
||||
export type GelBytesBuilderInitial<TName extends string> = GelBytesBuilder<{
|
||||
name: TName;
|
||||
dataType: 'buffer';
|
||||
columnType: 'GelBytes';
|
||||
data: Uint8Array;
|
||||
driverParam: Uint8Array | Buffer;
|
||||
enumValues: undefined;
|
||||
}>;
|
||||
|
||||
export class GelBytesBuilder<T extends ColumnBuilderBaseConfig<'buffer', 'GelBytes'>> extends GelColumnBuilder<T> {
|
||||
static override readonly [entityKind]: string = 'GelBytesBuilder';
|
||||
|
||||
constructor(name: T['name']) {
|
||||
super(name, 'buffer', 'GelBytes');
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
override build<TTableName extends string>(
|
||||
table: AnyGelTable<{ name: TTableName }>,
|
||||
): GelBytes<MakeColumnConfig<T, TTableName>> {
|
||||
return new GelBytes<MakeColumnConfig<T, TTableName>>(
|
||||
table,
|
||||
this.config as ColumnBuilderRuntimeConfig<any, any>,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class GelBytes<T extends ColumnBaseConfig<'buffer', 'GelBytes'>> extends GelColumn<T> {
|
||||
static override readonly [entityKind]: string = 'GelBytes';
|
||||
|
||||
getSQLType(): string {
|
||||
return 'bytea';
|
||||
}
|
||||
}
|
||||
|
||||
export function bytes(): GelBytesBuilderInitial<''>;
|
||||
export function bytes<TName extends string>(name: TName): GelBytesBuilderInitial<TName>;
|
||||
export function bytes(name?: string) {
|
||||
return new GelBytesBuilder(name ?? '');
|
||||
}
|
||||
@@ -0,0 +1,332 @@
|
||||
import type {
|
||||
ColumnBuilderBase,
|
||||
ColumnBuilderBaseConfig,
|
||||
ColumnBuilderExtraConfig,
|
||||
ColumnBuilderRuntimeConfig,
|
||||
ColumnDataType,
|
||||
HasGenerated,
|
||||
MakeColumnConfig,
|
||||
} from '~/column-builder.ts';
|
||||
import { ColumnBuilder } from '~/column-builder.ts';
|
||||
import type { ColumnBaseConfig } from '~/column.ts';
|
||||
import { Column } from '~/column.ts';
|
||||
import { entityKind } from '~/entity.ts';
|
||||
import type { Simplify, Update } from '~/utils.ts';
|
||||
|
||||
import type { ForeignKey, UpdateDeleteAction } from '~/gel-core/foreign-keys.ts';
|
||||
import { ForeignKeyBuilder } from '~/gel-core/foreign-keys.ts';
|
||||
import type { AnyGelTable, GelTable } from '~/gel-core/table.ts';
|
||||
import type { SQL } from '~/sql/sql.ts';
|
||||
import { iife } from '~/tracing-utils.ts';
|
||||
import type { GelIndexOpClass } from '../indexes.ts';
|
||||
import { uniqueKeyName } from '../unique-constraint.ts';
|
||||
|
||||
export interface ReferenceConfig {
|
||||
ref: () => GelColumn;
|
||||
actions: {
|
||||
onUpdate?: UpdateDeleteAction;
|
||||
onDelete?: UpdateDeleteAction;
|
||||
};
|
||||
}
|
||||
|
||||
export interface GelColumnBuilderBase<
|
||||
T extends ColumnBuilderBaseConfig<ColumnDataType, string> = ColumnBuilderBaseConfig<ColumnDataType, string>,
|
||||
TTypeConfig extends object = object,
|
||||
> extends ColumnBuilderBase<T, TTypeConfig & { dialect: 'gel' }> {}
|
||||
|
||||
export abstract class GelColumnBuilder<
|
||||
T extends ColumnBuilderBaseConfig<ColumnDataType, string> = ColumnBuilderBaseConfig<ColumnDataType, string>,
|
||||
TRuntimeConfig extends object = object,
|
||||
TTypeConfig extends object = object,
|
||||
TExtraConfig extends ColumnBuilderExtraConfig = ColumnBuilderExtraConfig,
|
||||
> extends ColumnBuilder<T, TRuntimeConfig, TTypeConfig & { dialect: 'gel' }, TExtraConfig>
|
||||
implements GelColumnBuilderBase<T, TTypeConfig>
|
||||
{
|
||||
private foreignKeyConfigs: ReferenceConfig[] = [];
|
||||
|
||||
static override readonly [entityKind]: string = 'GelColumnBuilder';
|
||||
|
||||
array<TSize extends number | undefined = undefined>(size?: TSize): GelArrayBuilder<
|
||||
& {
|
||||
name: T['name'];
|
||||
dataType: 'array';
|
||||
columnType: 'GelArray';
|
||||
data: T['data'][];
|
||||
driverParam: T['driverParam'][] | string;
|
||||
enumValues: T['enumValues'];
|
||||
size: TSize;
|
||||
baseBuilder: T;
|
||||
}
|
||||
& (T extends { notNull: true } ? { notNull: true } : {})
|
||||
& (T extends { hasDefault: true } ? { hasDefault: true } : {}),
|
||||
T
|
||||
> {
|
||||
return new GelArrayBuilder(this.config.name, this as GelColumnBuilder<any, any>, size as any);
|
||||
}
|
||||
|
||||
references(
|
||||
ref: ReferenceConfig['ref'],
|
||||
actions: ReferenceConfig['actions'] = {},
|
||||
): this {
|
||||
this.foreignKeyConfigs.push({ ref, actions });
|
||||
return this;
|
||||
}
|
||||
|
||||
unique(
|
||||
name?: string,
|
||||
config?: { nulls: 'distinct' | 'not distinct' },
|
||||
): this {
|
||||
this.config.isUnique = true;
|
||||
this.config.uniqueName = name;
|
||||
this.config.uniqueType = config?.nulls;
|
||||
return this;
|
||||
}
|
||||
|
||||
generatedAlwaysAs(as: SQL | T['data'] | (() => SQL)): HasGenerated<this, {
|
||||
type: 'always';
|
||||
}> {
|
||||
this.config.generated = {
|
||||
as,
|
||||
type: 'always',
|
||||
mode: 'stored',
|
||||
};
|
||||
return this as HasGenerated<this, {
|
||||
type: 'always';
|
||||
}>;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
buildForeignKeys(column: GelColumn, table: GelTable): ForeignKey[] {
|
||||
return this.foreignKeyConfigs.map(({ ref, actions }) => {
|
||||
return iife(
|
||||
(ref, actions) => {
|
||||
const builder = new ForeignKeyBuilder(() => {
|
||||
const foreignColumn = ref();
|
||||
return { columns: [column], foreignColumns: [foreignColumn] };
|
||||
});
|
||||
if (actions.onUpdate) {
|
||||
builder.onUpdate(actions.onUpdate);
|
||||
}
|
||||
if (actions.onDelete) {
|
||||
builder.onDelete(actions.onDelete);
|
||||
}
|
||||
return builder.build(table);
|
||||
},
|
||||
ref,
|
||||
actions,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
abstract build<TTableName extends string>(
|
||||
table: AnyGelTable<{ name: TTableName }>,
|
||||
): GelColumn<MakeColumnConfig<T, TTableName>>;
|
||||
|
||||
/** @internal */
|
||||
buildExtraConfigColumn<TTableName extends string>(
|
||||
table: AnyGelTable<{ name: TTableName }>,
|
||||
): GelExtraConfigColumn {
|
||||
return new GelExtraConfigColumn(table, this.config);
|
||||
}
|
||||
}
|
||||
|
||||
// To understand how to use `GelColumn` and `GelColumn`, see `Column` and `AnyColumn` documentation.
|
||||
export abstract class GelColumn<
|
||||
T extends ColumnBaseConfig<ColumnDataType, string> = ColumnBaseConfig<ColumnDataType, string>,
|
||||
TRuntimeConfig extends object = {},
|
||||
TTypeConfig extends object = {},
|
||||
> extends Column<T, TRuntimeConfig, TTypeConfig & { dialect: 'gel' }> {
|
||||
static override readonly [entityKind]: string = 'GelColumn';
|
||||
|
||||
constructor(
|
||||
override readonly table: GelTable,
|
||||
config: ColumnBuilderRuntimeConfig<T['data'], TRuntimeConfig>,
|
||||
) {
|
||||
if (!config.uniqueName) {
|
||||
config.uniqueName = uniqueKeyName(table, [config.name]);
|
||||
}
|
||||
super(table, config);
|
||||
}
|
||||
}
|
||||
|
||||
export type IndexedExtraConfigType = { order?: 'asc' | 'desc'; nulls?: 'first' | 'last'; opClass?: string };
|
||||
|
||||
export class GelExtraConfigColumn<
|
||||
T extends ColumnBaseConfig<ColumnDataType, string> = ColumnBaseConfig<ColumnDataType, string>,
|
||||
> extends GelColumn<T, IndexedExtraConfigType> {
|
||||
static override readonly [entityKind]: string = 'GelExtraConfigColumn';
|
||||
|
||||
override getSQLType(): string {
|
||||
return this.getSQLType();
|
||||
}
|
||||
|
||||
indexConfig: IndexedExtraConfigType = {
|
||||
order: this.config.order ?? 'asc',
|
||||
nulls: this.config.nulls ?? 'last',
|
||||
opClass: this.config.opClass,
|
||||
};
|
||||
defaultConfig: IndexedExtraConfigType = {
|
||||
order: 'asc',
|
||||
nulls: 'last',
|
||||
opClass: undefined,
|
||||
};
|
||||
|
||||
asc(): Omit<this, 'asc' | 'desc'> {
|
||||
this.indexConfig.order = 'asc';
|
||||
return this;
|
||||
}
|
||||
|
||||
desc(): Omit<this, 'asc' | 'desc'> {
|
||||
this.indexConfig.order = 'desc';
|
||||
return this;
|
||||
}
|
||||
|
||||
nullsFirst(): Omit<this, 'nullsFirst' | 'nullsLast'> {
|
||||
this.indexConfig.nulls = 'first';
|
||||
return this;
|
||||
}
|
||||
|
||||
nullsLast(): Omit<this, 'nullsFirst' | 'nullsLast'> {
|
||||
this.indexConfig.nulls = 'last';
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* ### PostgreSQL documentation quote
|
||||
*
|
||||
* > An operator class with optional parameters can be specified for each column of an index.
|
||||
* The operator class identifies the operators to be used by the index for that column.
|
||||
* For example, a B-tree index on four-byte integers would use the int4_ops class;
|
||||
* this operator class includes comparison functions for four-byte integers.
|
||||
* In practice the default operator class for the column's data type is usually sufficient.
|
||||
* The main point of having operator classes is that for some data types, there could be more than one meaningful ordering.
|
||||
* For example, we might want to sort a complex-number data type either by absolute value or by real part.
|
||||
* We could do this by defining two operator classes for the data type and then selecting the proper class when creating an index.
|
||||
* More information about operator classes check:
|
||||
*
|
||||
* ### Useful links
|
||||
* https://www.postgresql.org/docs/current/sql-createindex.html
|
||||
*
|
||||
* https://www.postgresql.org/docs/current/indexes-opclass.html
|
||||
*
|
||||
* https://www.postgresql.org/docs/current/xindex.html
|
||||
*
|
||||
* ### Additional types
|
||||
* If you have the `Gel_vector` extension installed in your database, you can use the
|
||||
* `vector_l2_ops`, `vector_ip_ops`, `vector_cosine_ops`, `vector_l1_ops`, `bit_hamming_ops`, `bit_jaccard_ops`, `halfvec_l2_ops`, `sparsevec_l2_ops` options, which are predefined types.
|
||||
*
|
||||
* **You can always specify any string you want in the operator class, in case Drizzle doesn't have it natively in its types**
|
||||
*
|
||||
* @param opClass
|
||||
* @returns
|
||||
*/
|
||||
op(opClass: GelIndexOpClass): Omit<this, 'op'> {
|
||||
this.indexConfig.opClass = opClass;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
export class IndexedColumn {
|
||||
static readonly [entityKind]: string = 'IndexedColumn';
|
||||
constructor(
|
||||
name: string | undefined,
|
||||
keyAsName: boolean,
|
||||
type: string,
|
||||
indexConfig: IndexedExtraConfigType,
|
||||
) {
|
||||
this.name = name;
|
||||
this.keyAsName = keyAsName;
|
||||
this.type = type;
|
||||
this.indexConfig = indexConfig;
|
||||
}
|
||||
|
||||
name: string | undefined;
|
||||
keyAsName: boolean;
|
||||
type: string;
|
||||
indexConfig: IndexedExtraConfigType;
|
||||
}
|
||||
|
||||
export type AnyGelColumn<TPartial extends Partial<ColumnBaseConfig<ColumnDataType, string>> = {}> = GelColumn<
|
||||
Required<Update<ColumnBaseConfig<ColumnDataType, string>, TPartial>>
|
||||
>;
|
||||
|
||||
export type GelArrayColumnBuilderBaseConfig = ColumnBuilderBaseConfig<'array', 'GelArray'> & {
|
||||
size: number | undefined;
|
||||
baseBuilder: ColumnBuilderBaseConfig<ColumnDataType, string>;
|
||||
};
|
||||
|
||||
export class GelArrayBuilder<
|
||||
T extends GelArrayColumnBuilderBaseConfig,
|
||||
TBase extends ColumnBuilderBaseConfig<ColumnDataType, string> | GelArrayColumnBuilderBaseConfig,
|
||||
> extends GelColumnBuilder<
|
||||
T,
|
||||
{
|
||||
baseBuilder: TBase extends GelArrayColumnBuilderBaseConfig ? GelArrayBuilder<
|
||||
TBase,
|
||||
TBase extends { baseBuilder: infer TBaseBuilder extends ColumnBuilderBaseConfig<any, any> } ? TBaseBuilder
|
||||
: never
|
||||
>
|
||||
: GelColumnBuilder<TBase, {}, Simplify<Omit<TBase, keyof ColumnBuilderBaseConfig<any, any>>>>;
|
||||
size: T['size'];
|
||||
},
|
||||
{
|
||||
baseBuilder: TBase extends GelArrayColumnBuilderBaseConfig ? GelArrayBuilder<
|
||||
TBase,
|
||||
TBase extends { baseBuilder: infer TBaseBuilder extends ColumnBuilderBaseConfig<any, any> } ? TBaseBuilder
|
||||
: never
|
||||
>
|
||||
: GelColumnBuilder<TBase, {}, Simplify<Omit<TBase, keyof ColumnBuilderBaseConfig<any, any>>>>;
|
||||
size: T['size'];
|
||||
}
|
||||
> {
|
||||
static override readonly [entityKind] = 'GelArrayBuilder';
|
||||
|
||||
constructor(
|
||||
name: string,
|
||||
baseBuilder: GelArrayBuilder<T, TBase>['config']['baseBuilder'],
|
||||
size: T['size'],
|
||||
) {
|
||||
super(name, 'array', 'GelArray');
|
||||
this.config.baseBuilder = baseBuilder;
|
||||
this.config.size = size;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
override build<TTableName extends string>(
|
||||
table: AnyGelTable<{ name: TTableName }>,
|
||||
): GelArray<MakeColumnConfig<T, TTableName> & { size: T['size']; baseBuilder: T['baseBuilder'] }, TBase> {
|
||||
const baseColumn = this.config.baseBuilder.build(table);
|
||||
return new GelArray<MakeColumnConfig<T, TTableName> & { size: T['size']; baseBuilder: T['baseBuilder'] }, TBase>(
|
||||
table as AnyGelTable<{ name: MakeColumnConfig<T, TTableName>['tableName'] }>,
|
||||
this.config as ColumnBuilderRuntimeConfig<any, any>,
|
||||
baseColumn,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class GelArray<
|
||||
T extends ColumnBaseConfig<'array', 'GelArray'> & {
|
||||
size: number | undefined;
|
||||
baseBuilder: ColumnBuilderBaseConfig<ColumnDataType, string>;
|
||||
},
|
||||
TBase extends ColumnBuilderBaseConfig<ColumnDataType, string>,
|
||||
> extends GelColumn<T, {}, { size: T['size']; baseBuilder: T['baseBuilder'] }> {
|
||||
readonly size: T['size'];
|
||||
|
||||
static override readonly [entityKind]: string = 'GelArray';
|
||||
|
||||
constructor(
|
||||
table: AnyGelTable<{ name: T['tableName'] }>,
|
||||
config: GelArrayBuilder<T, TBase>['config'],
|
||||
readonly baseColumn: GelColumn,
|
||||
readonly range?: [number | undefined, number | undefined],
|
||||
) {
|
||||
super(table, config);
|
||||
this.size = config.size;
|
||||
}
|
||||
|
||||
getSQLType(): string {
|
||||
return `${this.baseColumn.getSQLType()}[${typeof this.size === 'number' ? this.size : ''}]`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
import type { ColumnBuilderBaseConfig, ColumnBuilderRuntimeConfig, MakeColumnConfig } from '~/column-builder.ts';
|
||||
import type { ColumnBaseConfig } from '~/column.ts';
|
||||
import { entityKind } from '~/entity.ts';
|
||||
import type { AnyGelTable } from '~/gel-core/table.ts';
|
||||
import type { SQL } from '~/sql/sql.ts';
|
||||
import { type Equal, getColumnNameAndConfig } from '~/utils.ts';
|
||||
import { GelColumn, GelColumnBuilder } from './common.ts';
|
||||
|
||||
export type ConvertCustomConfig<TName extends string, T extends Partial<CustomTypeValues>> =
|
||||
& {
|
||||
name: TName;
|
||||
dataType: 'custom';
|
||||
columnType: 'GelCustomColumn';
|
||||
data: T['data'];
|
||||
driverParam: T['driverData'];
|
||||
enumValues: undefined;
|
||||
}
|
||||
& (T['notNull'] extends true ? { notNull: true } : {})
|
||||
& (T['default'] extends true ? { hasDefault: true } : {});
|
||||
|
||||
export interface GelCustomColumnInnerConfig {
|
||||
customTypeValues: CustomTypeValues;
|
||||
}
|
||||
|
||||
export class GelCustomColumnBuilder<T extends ColumnBuilderBaseConfig<'custom', 'GelCustomColumn'>>
|
||||
extends GelColumnBuilder<
|
||||
T,
|
||||
{
|
||||
fieldConfig: CustomTypeValues['config'];
|
||||
customTypeParams: CustomTypeParams<any>;
|
||||
},
|
||||
{
|
||||
gelColumnBuilderBrand: 'GelCustomColumnBuilderBrand';
|
||||
}
|
||||
>
|
||||
{
|
||||
static override readonly [entityKind]: string = 'GelCustomColumnBuilder';
|
||||
|
||||
constructor(
|
||||
name: T['name'],
|
||||
fieldConfig: CustomTypeValues['config'],
|
||||
customTypeParams: CustomTypeParams<any>,
|
||||
) {
|
||||
super(name, 'custom', 'GelCustomColumn');
|
||||
this.config.fieldConfig = fieldConfig;
|
||||
this.config.customTypeParams = customTypeParams;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
build<TTableName extends string>(
|
||||
table: AnyGelTable<{ name: TTableName }>,
|
||||
): GelCustomColumn<MakeColumnConfig<T, TTableName>> {
|
||||
return new GelCustomColumn<MakeColumnConfig<T, TTableName>>(
|
||||
table,
|
||||
this.config as ColumnBuilderRuntimeConfig<any, any>,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class GelCustomColumn<T extends ColumnBaseConfig<'custom', 'GelCustomColumn'>> extends GelColumn<T> {
|
||||
static override readonly [entityKind]: string = 'GelCustomColumn';
|
||||
|
||||
private sqlName: string;
|
||||
private mapTo?: (value: T['data']) => T['driverParam'];
|
||||
private mapFrom?: (value: T['driverParam']) => T['data'];
|
||||
|
||||
constructor(
|
||||
table: AnyGelTable<{ name: T['tableName'] }>,
|
||||
config: GelCustomColumnBuilder<T>['config'],
|
||||
) {
|
||||
super(table, config);
|
||||
this.sqlName = config.customTypeParams.dataType(config.fieldConfig);
|
||||
this.mapTo = config.customTypeParams.toDriver;
|
||||
this.mapFrom = config.customTypeParams.fromDriver;
|
||||
}
|
||||
|
||||
getSQLType(): string {
|
||||
return this.sqlName;
|
||||
}
|
||||
|
||||
override mapFromDriverValue(value: T['driverParam']): T['data'] {
|
||||
return typeof this.mapFrom === 'function' ? this.mapFrom(value) : value as T['data'];
|
||||
}
|
||||
|
||||
override mapToDriverValue(value: T['data']): T['driverParam'] {
|
||||
return typeof this.mapTo === 'function' ? this.mapTo(value) : value as T['data'];
|
||||
}
|
||||
}
|
||||
|
||||
export type CustomTypeValues = {
|
||||
/**
|
||||
* Required type for custom column, that will infer proper type model
|
||||
*
|
||||
* Examples:
|
||||
*
|
||||
* If you want your column to be `string` type after selecting/or on inserting - use `data: string`. Like `text`, `varchar`
|
||||
*
|
||||
* If you want your column to be `number` type after selecting/or on inserting - use `data: number`. Like `integer`
|
||||
*/
|
||||
data: unknown;
|
||||
|
||||
/**
|
||||
* Type helper, that represents what type database driver is accepting for specific database data type
|
||||
*/
|
||||
driverData?: unknown;
|
||||
|
||||
/**
|
||||
* What config type should be used for {@link CustomTypeParams} `dataType` generation
|
||||
*/
|
||||
config?: Record<string, any>;
|
||||
|
||||
/**
|
||||
* Whether the config argument should be required or not
|
||||
* @default false
|
||||
*/
|
||||
configRequired?: boolean;
|
||||
|
||||
/**
|
||||
* If your custom data type should be notNull by default you can use `notNull: true`
|
||||
*
|
||||
* @example
|
||||
* const customSerial = customType<{ data: number, notNull: true, default: true }>({
|
||||
* dataType() {
|
||||
* return 'serial';
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
notNull?: boolean;
|
||||
|
||||
/**
|
||||
* If your custom data type has default you can use `default: true`
|
||||
*
|
||||
* @example
|
||||
* const customSerial = customType<{ data: number, notNull: true, default: true }>({
|
||||
* dataType() {
|
||||
* return 'serial';
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
default?: boolean;
|
||||
};
|
||||
|
||||
export interface CustomTypeParams<T extends CustomTypeValues> {
|
||||
/**
|
||||
* Database data type string representation, that is used for migrations
|
||||
* @example
|
||||
* ```
|
||||
* `jsonb`, `text`
|
||||
* ```
|
||||
*
|
||||
* If database data type needs additional params you can use them from `config` param
|
||||
* @example
|
||||
* ```
|
||||
* `varchar(256)`, `numeric(2,3)`
|
||||
* ```
|
||||
*
|
||||
* To make `config` be of specific type please use config generic in {@link CustomTypeValues}
|
||||
*
|
||||
* @example
|
||||
* Usage example
|
||||
* ```
|
||||
* dataType() {
|
||||
* return 'boolean';
|
||||
* },
|
||||
* ```
|
||||
* Or
|
||||
* ```
|
||||
* dataType(config) {
|
||||
* return typeof config.length !== 'undefined' ? `varchar(${config.length})` : `varchar`;
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
dataType: (config: T['config'] | (Equal<T['configRequired'], true> extends true ? never : undefined)) => string;
|
||||
|
||||
/**
|
||||
* Optional mapping function, between user input and driver
|
||||
* @example
|
||||
* For example, when using jsonb we need to map JS/TS object to string before writing to database
|
||||
* ```
|
||||
* toDriver(value: TData): string {
|
||||
* return JSON.stringify(value);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
toDriver?: (value: T['data']) => T['driverData'] | SQL;
|
||||
|
||||
/**
|
||||
* Optional mapping function, that is responsible for data mapping from database to JS/TS code
|
||||
* @example
|
||||
* For example, when using timestamp we need to map string Date representation to JS Date
|
||||
* ```
|
||||
* fromDriver(value: string): Date {
|
||||
* return new Date(value);
|
||||
* },
|
||||
* ```
|
||||
*/
|
||||
fromDriver?: (value: T['driverData']) => T['data'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom gel database data type generator
|
||||
*/
|
||||
export function customType<T extends CustomTypeValues = CustomTypeValues>(
|
||||
customTypeParams: CustomTypeParams<T>,
|
||||
): Equal<T['configRequired'], true> extends true ? {
|
||||
<TConfig extends Record<string, any> & T['config']>(
|
||||
fieldConfig: TConfig,
|
||||
): GelCustomColumnBuilder<ConvertCustomConfig<'', T>>;
|
||||
<TName extends string>(
|
||||
dbName: TName,
|
||||
fieldConfig: T['config'],
|
||||
): GelCustomColumnBuilder<ConvertCustomConfig<TName, T>>;
|
||||
}
|
||||
: {
|
||||
(): GelCustomColumnBuilder<ConvertCustomConfig<'', T>>;
|
||||
<TConfig extends Record<string, any> & T['config']>(
|
||||
fieldConfig?: TConfig,
|
||||
): GelCustomColumnBuilder<ConvertCustomConfig<'', T>>;
|
||||
<TName extends string>(
|
||||
dbName: TName,
|
||||
fieldConfig?: T['config'],
|
||||
): GelCustomColumnBuilder<ConvertCustomConfig<TName, T>>;
|
||||
}
|
||||
{
|
||||
return <TName extends string>(
|
||||
a?: TName | T['config'],
|
||||
b?: T['config'],
|
||||
): GelCustomColumnBuilder<ConvertCustomConfig<TName, T>> => {
|
||||
const { name, config } = getColumnNameAndConfig<T['config']>(a, b);
|
||||
return new GelCustomColumnBuilder(name as ConvertCustomConfig<TName, T>['name'], config, customTypeParams);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import type { DateDuration } from 'gel';
|
||||
import type { ColumnBuilderBaseConfig, ColumnBuilderRuntimeConfig, MakeColumnConfig } from '~/column-builder.ts';
|
||||
import type { ColumnBaseConfig } from '~/column.ts';
|
||||
import { entityKind } from '~/entity.ts';
|
||||
import type { AnyGelTable } from '~/gel-core/table.ts';
|
||||
import { GelColumn, GelColumnBuilder } from './common.ts';
|
||||
|
||||
export type GelDateDurationBuilderInitial<TName extends string> = GelDateDurationBuilder<{
|
||||
name: TName;
|
||||
dataType: 'dateDuration';
|
||||
columnType: 'GelDateDuration';
|
||||
data: DateDuration;
|
||||
driverParam: DateDuration;
|
||||
enumValues: undefined;
|
||||
}>;
|
||||
|
||||
export class GelDateDurationBuilder<T extends ColumnBuilderBaseConfig<'dateDuration', 'GelDateDuration'>>
|
||||
extends GelColumnBuilder<T>
|
||||
{
|
||||
static override readonly [entityKind]: string = 'GelDateDurationBuilder';
|
||||
|
||||
constructor(
|
||||
name: T['name'],
|
||||
) {
|
||||
super(name, 'dateDuration', 'GelDateDuration');
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
override build<TTableName extends string>(
|
||||
table: AnyGelTable<{ name: TTableName }>,
|
||||
): GelDateDuration<MakeColumnConfig<T, TTableName>> {
|
||||
return new GelDateDuration<MakeColumnConfig<T, TTableName>>(
|
||||
table,
|
||||
this.config as ColumnBuilderRuntimeConfig<any, any>,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class GelDateDuration<T extends ColumnBaseConfig<'dateDuration', 'GelDateDuration'>> extends GelColumn<T> {
|
||||
static override readonly [entityKind]: string = 'GelDateDuration';
|
||||
|
||||
getSQLType(): string {
|
||||
return `dateDuration`;
|
||||
}
|
||||
}
|
||||
|
||||
export function dateDuration(): GelDateDurationBuilderInitial<''>;
|
||||
export function dateDuration<TName extends string>(name: TName): GelDateDurationBuilderInitial<TName>;
|
||||
export function dateDuration(name?: string) {
|
||||
return new GelDateDurationBuilder(name ?? '');
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { ColumnBuilderBaseConfig, ColumnDataType } from '~/column-builder.ts';
|
||||
import { entityKind } from '~/entity.ts';
|
||||
import { sql } from '~/sql/sql.ts';
|
||||
import { GelColumnBuilder } from './common.ts';
|
||||
|
||||
export abstract class GelLocalDateColumnBaseBuilder<
|
||||
T extends ColumnBuilderBaseConfig<ColumnDataType, string>,
|
||||
TRuntimeConfig extends object = object,
|
||||
> extends GelColumnBuilder<T, TRuntimeConfig> {
|
||||
static override readonly [entityKind]: string = 'GelLocalDateColumnBaseBuilder';
|
||||
|
||||
defaultNow() {
|
||||
return this.default(sql`now()`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import type { ColumnBuilderBaseConfig, ColumnBuilderRuntimeConfig, MakeColumnConfig } from '~/column-builder.ts';
|
||||
import type { ColumnBaseConfig } from '~/column.ts';
|
||||
import { entityKind } from '~/entity.ts';
|
||||
import type { AnyGelTable } from '~/gel-core/table.ts';
|
||||
import { GelColumn, GelColumnBuilder } from './common.ts';
|
||||
|
||||
export type GelDecimalBuilderInitial<TName extends string> = GelDecimalBuilder<{
|
||||
name: TName;
|
||||
dataType: 'string';
|
||||
columnType: 'GelDecimal';
|
||||
data: string;
|
||||
driverParam: string;
|
||||
enumValues: undefined;
|
||||
}>;
|
||||
|
||||
export class GelDecimalBuilder<T extends ColumnBuilderBaseConfig<'string', 'GelDecimal'>> extends GelColumnBuilder<
|
||||
T
|
||||
> {
|
||||
static override readonly [entityKind]: string = 'GelDecimalBuilder';
|
||||
|
||||
constructor(name: T['name']) {
|
||||
super(name, 'string', 'GelDecimal');
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
override build<TTableName extends string>(
|
||||
table: AnyGelTable<{ name: TTableName }>,
|
||||
): GelDecimal<MakeColumnConfig<T, TTableName>> {
|
||||
return new GelDecimal<MakeColumnConfig<T, TTableName>>(table, this.config as ColumnBuilderRuntimeConfig<any, any>);
|
||||
}
|
||||
}
|
||||
|
||||
export class GelDecimal<T extends ColumnBaseConfig<'string', 'GelDecimal'>> extends GelColumn<T> {
|
||||
static override readonly [entityKind]: string = 'GelDecimal';
|
||||
|
||||
constructor(table: AnyGelTable<{ name: T['tableName'] }>, config: GelDecimalBuilder<T>['config']) {
|
||||
super(table, config);
|
||||
}
|
||||
|
||||
getSQLType(): string {
|
||||
return 'numeric';
|
||||
}
|
||||
}
|
||||
|
||||
export function decimal(): GelDecimalBuilderInitial<''>;
|
||||
export function decimal<TName extends string>(name: TName): GelDecimalBuilderInitial<TName>;
|
||||
export function decimal(name?: string) {
|
||||
return new GelDecimalBuilder(name ?? '');
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import type { ColumnBuilderBaseConfig, ColumnBuilderRuntimeConfig, MakeColumnConfig } from '~/column-builder.ts';
|
||||
import type { ColumnBaseConfig } from '~/column.ts';
|
||||
import { entityKind } from '~/entity.ts';
|
||||
import type { AnyGelTable } from '~/gel-core/table.ts';
|
||||
import { GelColumn, GelColumnBuilder } from './common.ts';
|
||||
|
||||
export type GelDoublePrecisionBuilderInitial<TName extends string> = GelDoublePrecisionBuilder<{
|
||||
name: TName;
|
||||
dataType: 'number';
|
||||
columnType: 'GelDoublePrecision';
|
||||
data: number;
|
||||
driverParam: number;
|
||||
enumValues: undefined;
|
||||
}>;
|
||||
|
||||
export class GelDoublePrecisionBuilder<T extends ColumnBuilderBaseConfig<'number', 'GelDoublePrecision'>>
|
||||
extends GelColumnBuilder<T>
|
||||
{
|
||||
static override readonly [entityKind]: string = 'GelDoublePrecisionBuilder';
|
||||
|
||||
constructor(name: T['name']) {
|
||||
super(name, 'number', 'GelDoublePrecision');
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
override build<TTableName extends string>(
|
||||
table: AnyGelTable<{ name: TTableName }>,
|
||||
): GelDoublePrecision<MakeColumnConfig<T, TTableName>> {
|
||||
return new GelDoublePrecision<MakeColumnConfig<T, TTableName>>(
|
||||
table,
|
||||
this.config as ColumnBuilderRuntimeConfig<any, any>,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class GelDoublePrecision<T extends ColumnBaseConfig<'number', 'GelDoublePrecision'>> extends GelColumn<T> {
|
||||
static override readonly [entityKind]: string = 'GelDoublePrecision';
|
||||
|
||||
getSQLType(): string {
|
||||
return 'double precision';
|
||||
}
|
||||
|
||||
override mapFromDriverValue(value: string | number): number {
|
||||
if (typeof value === 'string') {
|
||||
return Number.parseFloat(value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
export function doublePrecision(): GelDoublePrecisionBuilderInitial<''>;
|
||||
export function doublePrecision<TName extends string>(name: TName): GelDoublePrecisionBuilderInitial<TName>;
|
||||
export function doublePrecision(name?: string) {
|
||||
return new GelDoublePrecisionBuilder(name ?? '');
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import type { Duration } from 'gel';
|
||||
import type { ColumnBuilderBaseConfig, ColumnBuilderRuntimeConfig, MakeColumnConfig } from '~/column-builder.ts';
|
||||
import type { ColumnBaseConfig } from '~/column.ts';
|
||||
import { entityKind } from '~/entity.ts';
|
||||
import type { AnyGelTable } from '~/gel-core/table.ts';
|
||||
import { GelColumn, GelColumnBuilder } from './common.ts';
|
||||
|
||||
export type GelDurationBuilderInitial<TName extends string> = GelDurationBuilder<{
|
||||
name: TName;
|
||||
dataType: 'duration';
|
||||
columnType: 'GelDuration';
|
||||
data: Duration;
|
||||
driverParam: Duration;
|
||||
enumValues: undefined;
|
||||
}>;
|
||||
|
||||
export class GelDurationBuilder<T extends ColumnBuilderBaseConfig<'duration', 'GelDuration'>>
|
||||
extends GelColumnBuilder<T>
|
||||
{
|
||||
static override readonly [entityKind]: string = 'GelDurationBuilder';
|
||||
|
||||
constructor(
|
||||
name: T['name'],
|
||||
) {
|
||||
super(name, 'duration', 'GelDuration');
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
override build<TTableName extends string>(
|
||||
table: AnyGelTable<{ name: TTableName }>,
|
||||
): GelDuration<MakeColumnConfig<T, TTableName>> {
|
||||
return new GelDuration<MakeColumnConfig<T, TTableName>>(table, this.config as ColumnBuilderRuntimeConfig<any, any>);
|
||||
}
|
||||
}
|
||||
|
||||
export class GelDuration<T extends ColumnBaseConfig<'duration', 'GelDuration'>> extends GelColumn<T> {
|
||||
static override readonly [entityKind]: string = 'GelDuration';
|
||||
|
||||
getSQLType(): string {
|
||||
return `duration`;
|
||||
}
|
||||
}
|
||||
|
||||
export function duration(): GelDurationBuilderInitial<''>;
|
||||
export function duration<TName extends string>(name: TName): GelDurationBuilderInitial<TName>;
|
||||
export function duration(name?: string) {
|
||||
return new GelDurationBuilder(name ?? '');
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
export * from './bigint.ts';
|
||||
export * from './bigintT.ts';
|
||||
export * from './boolean.ts';
|
||||
export * from './bytes.ts';
|
||||
export * from './common.ts';
|
||||
export * from './custom.ts';
|
||||
export * from './date-duration.ts';
|
||||
export * from './decimal.ts';
|
||||
export * from './double-precision.ts';
|
||||
export * from './duration.ts';
|
||||
export * from './int.common.ts';
|
||||
export * from './integer.ts';
|
||||
export * from './json.ts';
|
||||
export * from './localdate.ts';
|
||||
export * from './localtime.ts';
|
||||
export * from './real.ts';
|
||||
export * from './relative-duration.ts';
|
||||
export * from './smallint.ts';
|
||||
export * from './text.ts';
|
||||
export * from './timestamp.ts';
|
||||
export * from './timestamptz.ts';
|
||||
export * from './uuid.ts';
|
||||
@@ -0,0 +1,57 @@
|
||||
import type { ColumnBuilderBaseConfig, ColumnDataType, GeneratedIdentityConfig, IsIdentity } from '~/column-builder.ts';
|
||||
import { entityKind } from '~/entity.ts';
|
||||
import type { GelSequenceOptions } from '../sequence.ts';
|
||||
import { GelColumnBuilder } from './common.ts';
|
||||
|
||||
export abstract class GelIntColumnBaseBuilder<
|
||||
T extends ColumnBuilderBaseConfig<ColumnDataType, string>,
|
||||
> extends GelColumnBuilder<
|
||||
T,
|
||||
{ generatedIdentity: GeneratedIdentityConfig }
|
||||
> {
|
||||
static override readonly [entityKind]: string = 'GelIntColumnBaseBuilder';
|
||||
|
||||
generatedAlwaysAsIdentity(
|
||||
sequence?: GelSequenceOptions & { name?: string },
|
||||
): IsIdentity<this, 'always'> {
|
||||
if (sequence) {
|
||||
const { name, ...options } = sequence;
|
||||
this.config.generatedIdentity = {
|
||||
type: 'always',
|
||||
sequenceName: name,
|
||||
sequenceOptions: options,
|
||||
};
|
||||
} else {
|
||||
this.config.generatedIdentity = {
|
||||
type: 'always',
|
||||
};
|
||||
}
|
||||
|
||||
this.config.hasDefault = true;
|
||||
this.config.notNull = true;
|
||||
|
||||
return this as IsIdentity<this, 'always'>;
|
||||
}
|
||||
|
||||
generatedByDefaultAsIdentity(
|
||||
sequence?: GelSequenceOptions & { name?: string },
|
||||
): IsIdentity<this, 'byDefault'> {
|
||||
if (sequence) {
|
||||
const { name, ...options } = sequence;
|
||||
this.config.generatedIdentity = {
|
||||
type: 'byDefault',
|
||||
sequenceName: name,
|
||||
sequenceOptions: options,
|
||||
};
|
||||
} else {
|
||||
this.config.generatedIdentity = {
|
||||
type: 'byDefault',
|
||||
};
|
||||
}
|
||||
|
||||
this.config.hasDefault = true;
|
||||
this.config.notNull = true;
|
||||
|
||||
return this as IsIdentity<this, 'byDefault'>;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import type { ColumnBuilderBaseConfig, ColumnBuilderRuntimeConfig, MakeColumnConfig } from '~/column-builder.ts';
|
||||
import type { ColumnBaseConfig } from '~/column.ts';
|
||||
import { entityKind } from '~/entity.ts';
|
||||
import type { AnyGelTable } from '../table.ts';
|
||||
import { GelColumn } from './common.ts';
|
||||
import { GelIntColumnBaseBuilder } from './int.common.ts';
|
||||
|
||||
export type GelIntegerBuilderInitial<TName extends string> = GelIntegerBuilder<{
|
||||
name: TName;
|
||||
dataType: 'number';
|
||||
columnType: 'GelInteger';
|
||||
data: number;
|
||||
driverParam: number;
|
||||
enumValues: undefined;
|
||||
}>;
|
||||
|
||||
export class GelIntegerBuilder<T extends ColumnBuilderBaseConfig<'number', 'GelInteger'>>
|
||||
extends GelIntColumnBaseBuilder<T>
|
||||
{
|
||||
static override readonly [entityKind]: string = 'GelIntegerBuilder';
|
||||
|
||||
constructor(name: T['name']) {
|
||||
super(name, 'number', 'GelInteger');
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
override build<TTableName extends string>(
|
||||
table: AnyGelTable<{ name: TTableName }>,
|
||||
): GelInteger<MakeColumnConfig<T, TTableName>> {
|
||||
return new GelInteger<MakeColumnConfig<T, TTableName>>(table, this.config as ColumnBuilderRuntimeConfig<any, any>);
|
||||
}
|
||||
}
|
||||
|
||||
export class GelInteger<T extends ColumnBaseConfig<'number', 'GelInteger'>> extends GelColumn<T> {
|
||||
static override readonly [entityKind]: string = 'GelInteger';
|
||||
|
||||
getSQLType(): string {
|
||||
return 'integer';
|
||||
}
|
||||
}
|
||||
|
||||
export function integer(): GelIntegerBuilderInitial<''>;
|
||||
export function integer<TName extends string>(name: TName): GelIntegerBuilderInitial<TName>;
|
||||
export function integer(name?: string) {
|
||||
return new GelIntegerBuilder(name ?? '');
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import type { ColumnBuilderBaseConfig, ColumnBuilderRuntimeConfig, MakeColumnConfig } from '~/column-builder.ts';
|
||||
import type { ColumnBaseConfig } from '~/column.ts';
|
||||
import { entityKind } from '~/entity.ts';
|
||||
import type { AnyGelTable } from '~/gel-core/table.ts';
|
||||
import { GelColumn, GelColumnBuilder } from './common.ts';
|
||||
|
||||
export type GelJsonBuilderInitial<TName extends string> = GelJsonBuilder<{
|
||||
name: TName;
|
||||
dataType: 'json';
|
||||
columnType: 'GelJson';
|
||||
data: unknown;
|
||||
driverParam: unknown;
|
||||
enumValues: undefined;
|
||||
}>;
|
||||
|
||||
export class GelJsonBuilder<T extends ColumnBuilderBaseConfig<'json', 'GelJson'>> extends GelColumnBuilder<
|
||||
T
|
||||
> {
|
||||
static override readonly [entityKind]: string = 'GelJsonBuilder';
|
||||
|
||||
constructor(name: T['name']) {
|
||||
super(name, 'json', 'GelJson');
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
override build<TTableName extends string>(
|
||||
table: AnyGelTable<{ name: TTableName }>,
|
||||
): GelJson<MakeColumnConfig<T, TTableName>> {
|
||||
return new GelJson<MakeColumnConfig<T, TTableName>>(table, this.config as ColumnBuilderRuntimeConfig<any, any>);
|
||||
}
|
||||
}
|
||||
|
||||
export class GelJson<T extends ColumnBaseConfig<'json', 'GelJson'>> extends GelColumn<T> {
|
||||
static override readonly [entityKind]: string = 'GelJson';
|
||||
|
||||
constructor(table: AnyGelTable<{ name: T['tableName'] }>, config: GelJsonBuilder<T>['config']) {
|
||||
super(table, config);
|
||||
}
|
||||
|
||||
getSQLType(): string {
|
||||
return 'json';
|
||||
}
|
||||
}
|
||||
|
||||
export function json(): GelJsonBuilderInitial<''>;
|
||||
export function json<TName extends string>(name: TName): GelJsonBuilderInitial<TName>;
|
||||
export function json(name?: string) {
|
||||
return new GelJsonBuilder(name ?? '');
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import type { LocalDate } from 'gel';
|
||||
import type { ColumnBuilderBaseConfig, ColumnBuilderRuntimeConfig, MakeColumnConfig } from '~/column-builder.ts';
|
||||
import type { ColumnBaseConfig } from '~/column.ts';
|
||||
import { entityKind } from '~/entity.ts';
|
||||
import type { AnyGelTable } from '~/gel-core/table.ts';
|
||||
import { GelColumn } from './common.ts';
|
||||
import { GelLocalDateColumnBaseBuilder } from './date.common.ts';
|
||||
|
||||
export type GelLocalDateStringBuilderInitial<TName extends string> = GelLocalDateStringBuilder<{
|
||||
name: TName;
|
||||
dataType: 'localDate';
|
||||
columnType: 'GelLocalDateString';
|
||||
data: LocalDate;
|
||||
driverParam: LocalDate;
|
||||
enumValues: undefined;
|
||||
}>;
|
||||
|
||||
export class GelLocalDateStringBuilder<T extends ColumnBuilderBaseConfig<'localDate', 'GelLocalDateString'>>
|
||||
extends GelLocalDateColumnBaseBuilder<T>
|
||||
{
|
||||
static override readonly [entityKind]: string = 'GelLocalDateStringBuilder';
|
||||
|
||||
constructor(name: T['name']) {
|
||||
super(name, 'localDate', 'GelLocalDateString');
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
override build<TTableName extends string>(
|
||||
table: AnyGelTable<{ name: TTableName }>,
|
||||
): GelLocalDateString<MakeColumnConfig<T, TTableName>> {
|
||||
return new GelLocalDateString<MakeColumnConfig<T, TTableName>>(
|
||||
table,
|
||||
this.config as ColumnBuilderRuntimeConfig<any, any>,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class GelLocalDateString<T extends ColumnBaseConfig<'localDate', 'GelLocalDateString'>> extends GelColumn<T> {
|
||||
static override readonly [entityKind]: string = 'GelLocalDateString';
|
||||
|
||||
getSQLType(): string {
|
||||
return 'cal::local_date';
|
||||
}
|
||||
}
|
||||
|
||||
export function localDate(): GelLocalDateStringBuilderInitial<''>;
|
||||
export function localDate<TName extends string>(name: TName): GelLocalDateStringBuilderInitial<TName>;
|
||||
export function localDate(name?: string) {
|
||||
return new GelLocalDateStringBuilder(name ?? '');
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import type { LocalTime } from 'gel';
|
||||
import type { ColumnBuilderBaseConfig, ColumnBuilderRuntimeConfig, MakeColumnConfig } from '~/column-builder.ts';
|
||||
import type { ColumnBaseConfig } from '~/column.ts';
|
||||
import { entityKind } from '~/entity.ts';
|
||||
import type { AnyGelTable } from '~/gel-core/table.ts';
|
||||
import { GelColumn } from './common.ts';
|
||||
import { GelLocalDateColumnBaseBuilder } from './date.common.ts';
|
||||
|
||||
export type GelLocalTimeBuilderInitial<TName extends string> = GelLocalTimeBuilder<{
|
||||
name: TName;
|
||||
dataType: 'localTime';
|
||||
columnType: 'GelLocalTime';
|
||||
data: LocalTime;
|
||||
driverParam: LocalTime;
|
||||
enumValues: undefined;
|
||||
}>;
|
||||
|
||||
export class GelLocalTimeBuilder<T extends ColumnBuilderBaseConfig<'localTime', 'GelLocalTime'>>
|
||||
extends GelLocalDateColumnBaseBuilder<T>
|
||||
{
|
||||
static override readonly [entityKind]: string = 'GelLocalTimeBuilder';
|
||||
|
||||
constructor(name: T['name']) {
|
||||
super(name, 'localTime', 'GelLocalTime');
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
override build<TTableName extends string>(
|
||||
table: AnyGelTable<{ name: TTableName }>,
|
||||
): GelLocalTime<MakeColumnConfig<T, TTableName>> {
|
||||
return new GelLocalTime<MakeColumnConfig<T, TTableName>>(
|
||||
table,
|
||||
this.config as ColumnBuilderRuntimeConfig<any, any>,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class GelLocalTime<T extends ColumnBaseConfig<'localTime', 'GelLocalTime'>> extends GelColumn<T> {
|
||||
static override readonly [entityKind]: string = 'GelLocalTime';
|
||||
|
||||
getSQLType(): string {
|
||||
return 'cal::local_time';
|
||||
}
|
||||
}
|
||||
|
||||
export function localTime(): GelLocalTimeBuilderInitial<''>;
|
||||
export function localTime<TName extends string>(name: TName): GelLocalTimeBuilderInitial<TName>;
|
||||
export function localTime(name?: string) {
|
||||
return new GelLocalTimeBuilder(name ?? '');
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import type { ColumnBuilderBaseConfig, ColumnBuilderRuntimeConfig, MakeColumnConfig } from '~/column-builder.ts';
|
||||
import type { ColumnBaseConfig } from '~/column.ts';
|
||||
import { entityKind } from '~/entity.ts';
|
||||
import type { AnyGelTable } from '~/gel-core/table.ts';
|
||||
import { GelColumn, GelColumnBuilder } from './common.ts';
|
||||
|
||||
export type GelRealBuilderInitial<TName extends string> = GelRealBuilder<{
|
||||
name: TName;
|
||||
dataType: 'number';
|
||||
columnType: 'GelReal';
|
||||
data: number;
|
||||
driverParam: number;
|
||||
enumValues: undefined;
|
||||
}>;
|
||||
|
||||
export class GelRealBuilder<T extends ColumnBuilderBaseConfig<'number', 'GelReal'>> extends GelColumnBuilder<
|
||||
T,
|
||||
{ length: number | undefined }
|
||||
> {
|
||||
static override readonly [entityKind]: string = 'GelRealBuilder';
|
||||
|
||||
constructor(name: T['name'], length?: number) {
|
||||
super(name, 'number', 'GelReal');
|
||||
this.config.length = length;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
override build<TTableName extends string>(
|
||||
table: AnyGelTable<{ name: TTableName }>,
|
||||
): GelReal<MakeColumnConfig<T, TTableName>> {
|
||||
return new GelReal<MakeColumnConfig<T, TTableName>>(table, this.config as ColumnBuilderRuntimeConfig<any, any>);
|
||||
}
|
||||
}
|
||||
|
||||
export class GelReal<T extends ColumnBaseConfig<'number', 'GelReal'>> extends GelColumn<T> {
|
||||
static override readonly [entityKind]: string = 'GelReal';
|
||||
|
||||
constructor(table: AnyGelTable<{ name: T['tableName'] }>, config: GelRealBuilder<T>['config']) {
|
||||
super(table, config);
|
||||
}
|
||||
|
||||
getSQLType(): string {
|
||||
return 'real';
|
||||
}
|
||||
}
|
||||
|
||||
export function real(): GelRealBuilderInitial<''>;
|
||||
export function real<TName extends string>(name: TName): GelRealBuilderInitial<TName>;
|
||||
export function real(name?: string) {
|
||||
return new GelRealBuilder(name ?? '');
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import type { RelativeDuration } from 'gel';
|
||||
import type { ColumnBuilderBaseConfig, ColumnBuilderRuntimeConfig, MakeColumnConfig } from '~/column-builder.ts';
|
||||
import type { ColumnBaseConfig } from '~/column.ts';
|
||||
import { entityKind } from '~/entity.ts';
|
||||
import type { AnyGelTable } from '~/gel-core/table.ts';
|
||||
import { GelColumn, GelColumnBuilder } from './common.ts';
|
||||
|
||||
export type GelRelDurationBuilderInitial<TName extends string> = GelRelDurationBuilder<{
|
||||
name: TName;
|
||||
dataType: 'relDuration';
|
||||
columnType: 'GelRelDuration';
|
||||
data: RelativeDuration;
|
||||
driverParam: RelativeDuration;
|
||||
enumValues: undefined;
|
||||
}>;
|
||||
|
||||
export class GelRelDurationBuilder<T extends ColumnBuilderBaseConfig<'relDuration', 'GelRelDuration'>>
|
||||
extends GelColumnBuilder<T>
|
||||
{
|
||||
static override readonly [entityKind]: string = 'GelRelDurationBuilder';
|
||||
|
||||
constructor(
|
||||
name: T['name'],
|
||||
) {
|
||||
super(name, 'relDuration', 'GelRelDuration');
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
override build<TTableName extends string>(
|
||||
table: AnyGelTable<{ name: TTableName }>,
|
||||
): GelRelDuration<MakeColumnConfig<T, TTableName>> {
|
||||
return new GelRelDuration<MakeColumnConfig<T, TTableName>>(
|
||||
table,
|
||||
this.config as ColumnBuilderRuntimeConfig<any, any>,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class GelRelDuration<T extends ColumnBaseConfig<'relDuration', 'GelRelDuration'>> extends GelColumn<T> {
|
||||
static override readonly [entityKind]: string = 'GelRelDuration';
|
||||
|
||||
getSQLType(): string {
|
||||
return `edgedbt.relative_duration_t`;
|
||||
}
|
||||
}
|
||||
|
||||
export function relDuration(): GelRelDurationBuilderInitial<''>;
|
||||
export function relDuration<TName extends string>(name: TName): GelRelDurationBuilderInitial<TName>;
|
||||
export function relDuration(name?: string) {
|
||||
return new GelRelDurationBuilder(name ?? '');
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import type { ColumnBuilderBaseConfig, ColumnBuilderRuntimeConfig, MakeColumnConfig } from '~/column-builder.ts';
|
||||
import type { ColumnBaseConfig } from '~/column.ts';
|
||||
import { entityKind } from '~/entity.ts';
|
||||
import type { AnyGelTable } from '~/gel-core/table.ts';
|
||||
import { GelColumn } from './common.ts';
|
||||
import { GelIntColumnBaseBuilder } from './int.common.ts';
|
||||
|
||||
export type GelSmallIntBuilderInitial<TName extends string> = GelSmallIntBuilder<{
|
||||
name: TName;
|
||||
dataType: 'number';
|
||||
columnType: 'GelSmallInt';
|
||||
data: number;
|
||||
driverParam: number;
|
||||
enumValues: undefined;
|
||||
}>;
|
||||
|
||||
export class GelSmallIntBuilder<T extends ColumnBuilderBaseConfig<'number', 'GelSmallInt'>>
|
||||
extends GelIntColumnBaseBuilder<T>
|
||||
{
|
||||
static override readonly [entityKind]: string = 'GelSmallIntBuilder';
|
||||
|
||||
constructor(name: T['name']) {
|
||||
super(name, 'number', 'GelSmallInt');
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
override build<TTableName extends string>(
|
||||
table: AnyGelTable<{ name: TTableName }>,
|
||||
): GelSmallInt<MakeColumnConfig<T, TTableName>> {
|
||||
return new GelSmallInt<MakeColumnConfig<T, TTableName>>(table, this.config as ColumnBuilderRuntimeConfig<any, any>);
|
||||
}
|
||||
}
|
||||
|
||||
export class GelSmallInt<T extends ColumnBaseConfig<'number', 'GelSmallInt'>> extends GelColumn<T> {
|
||||
static override readonly [entityKind]: string = 'GelSmallInt';
|
||||
|
||||
getSQLType(): string {
|
||||
return 'smallint';
|
||||
}
|
||||
}
|
||||
|
||||
export function smallint(): GelSmallIntBuilderInitial<''>;
|
||||
export function smallint<TName extends string>(name: TName): GelSmallIntBuilderInitial<TName>;
|
||||
export function smallint(name?: string) {
|
||||
return new GelSmallIntBuilder(name ?? '');
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import type { ColumnBuilderBaseConfig, ColumnBuilderRuntimeConfig, MakeColumnConfig } from '~/column-builder.ts';
|
||||
import type { ColumnBaseConfig } from '~/column.ts';
|
||||
import { entityKind } from '~/entity.ts';
|
||||
import type { AnyGelTable } from '~/gel-core/table.ts';
|
||||
import { GelColumn, GelColumnBuilder } from './common.ts';
|
||||
|
||||
type GelTextBuilderInitial<TName extends string> = GelTextBuilder<{
|
||||
name: TName;
|
||||
dataType: 'string';
|
||||
columnType: 'GelText';
|
||||
data: string;
|
||||
driverParam: string;
|
||||
enumValues: undefined;
|
||||
}>;
|
||||
|
||||
export class GelTextBuilder<
|
||||
T extends ColumnBuilderBaseConfig<'string', 'GelText'>,
|
||||
> extends GelColumnBuilder<T> {
|
||||
static override readonly [entityKind]: string = 'GelTextBuilder';
|
||||
|
||||
constructor(
|
||||
name: T['name'],
|
||||
) {
|
||||
super(name, 'string', 'GelText');
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
override build<TTableName extends string>(
|
||||
table: AnyGelTable<{ name: TTableName }>,
|
||||
): GelText<MakeColumnConfig<T, TTableName>> {
|
||||
return new GelText<MakeColumnConfig<T, TTableName>>(table, this.config as ColumnBuilderRuntimeConfig<any, any>);
|
||||
}
|
||||
}
|
||||
|
||||
export class GelText<T extends ColumnBaseConfig<'string', 'GelText'>>
|
||||
extends GelColumn<T, { enumValues: T['enumValues'] }>
|
||||
{
|
||||
static override readonly [entityKind]: string = 'GelText';
|
||||
|
||||
override readonly enumValues = this.config.enumValues;
|
||||
|
||||
getSQLType(): string {
|
||||
return 'text';
|
||||
}
|
||||
}
|
||||
|
||||
export function text(): GelTextBuilderInitial<''>;
|
||||
export function text<TName extends string>(name: TName): GelTextBuilderInitial<TName>;
|
||||
export function text(name?: string): any {
|
||||
return new GelTextBuilder(name ?? '');
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import type { LocalDateTime } from 'gel';
|
||||
import type { ColumnBuilderBaseConfig, ColumnBuilderRuntimeConfig, MakeColumnConfig } from '~/column-builder.ts';
|
||||
import type { ColumnBaseConfig } from '~/column.ts';
|
||||
import { entityKind } from '~/entity.ts';
|
||||
import type { AnyGelTable } from '~/gel-core/table.ts';
|
||||
import { GelColumn } from './common.ts';
|
||||
import { GelLocalDateColumnBaseBuilder } from './date.common.ts';
|
||||
|
||||
export type GelTimestampBuilderInitial<TName extends string> = GelTimestampBuilder<{
|
||||
name: TName;
|
||||
dataType: 'localDateTime';
|
||||
columnType: 'GelTimestamp';
|
||||
data: LocalDateTime;
|
||||
driverParam: LocalDateTime;
|
||||
enumValues: undefined;
|
||||
}>;
|
||||
|
||||
export class GelTimestampBuilder<T extends ColumnBuilderBaseConfig<'localDateTime', 'GelTimestamp'>>
|
||||
extends GelLocalDateColumnBaseBuilder<
|
||||
T
|
||||
>
|
||||
{
|
||||
static override readonly [entityKind]: string = 'GelTimestampBuilder';
|
||||
|
||||
constructor(
|
||||
name: T['name'],
|
||||
) {
|
||||
super(name, 'localDateTime', 'GelTimestamp');
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
override build<TTableName extends string>(
|
||||
table: AnyGelTable<{ name: TTableName }>,
|
||||
): GelTimestamp<MakeColumnConfig<T, TTableName>> {
|
||||
return new GelTimestamp<MakeColumnConfig<T, TTableName>>(
|
||||
table,
|
||||
this.config as ColumnBuilderRuntimeConfig<any, any>,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class GelTimestamp<T extends ColumnBaseConfig<'localDateTime', 'GelTimestamp'>> extends GelColumn<T> {
|
||||
static override readonly [entityKind]: string = 'GelTimestamp';
|
||||
|
||||
constructor(table: AnyGelTable<{ name: T['tableName'] }>, config: GelTimestampBuilder<T>['config']) {
|
||||
super(table, config);
|
||||
}
|
||||
|
||||
getSQLType(): string {
|
||||
return 'cal::local_datetime';
|
||||
}
|
||||
}
|
||||
|
||||
export function timestamp(): GelTimestampBuilderInitial<''>;
|
||||
export function timestamp<TName extends string>(
|
||||
name: TName,
|
||||
): GelTimestampBuilderInitial<TName>;
|
||||
export function timestamp(name?: string) {
|
||||
return new GelTimestampBuilder(name ?? '');
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import type { ColumnBuilderBaseConfig, ColumnBuilderRuntimeConfig, MakeColumnConfig } from '~/column-builder.ts';
|
||||
import type { ColumnBaseConfig } from '~/column.ts';
|
||||
import { entityKind } from '~/entity.ts';
|
||||
import type { AnyGelTable } from '~/gel-core/table.ts';
|
||||
import { GelColumn } from './common.ts';
|
||||
import { GelLocalDateColumnBaseBuilder } from './date.common.ts';
|
||||
|
||||
export type GelTimestampTzBuilderInitial<TName extends string> = GelTimestampTzBuilder<{
|
||||
name: TName;
|
||||
dataType: 'date';
|
||||
columnType: 'GelTimestampTz';
|
||||
data: Date;
|
||||
driverParam: Date;
|
||||
enumValues: undefined;
|
||||
}>;
|
||||
|
||||
export class GelTimestampTzBuilder<T extends ColumnBuilderBaseConfig<'date', 'GelTimestampTz'>>
|
||||
extends GelLocalDateColumnBaseBuilder<
|
||||
T
|
||||
>
|
||||
{
|
||||
static override readonly [entityKind]: string = 'GelTimestampTzBuilder';
|
||||
|
||||
constructor(
|
||||
name: T['name'],
|
||||
) {
|
||||
super(name, 'date', 'GelTimestampTz');
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
override build<TTableName extends string>(
|
||||
table: AnyGelTable<{ name: TTableName }>,
|
||||
): GelTimestampTz<MakeColumnConfig<T, TTableName>> {
|
||||
return new GelTimestampTz<MakeColumnConfig<T, TTableName>>(
|
||||
table,
|
||||
this.config as ColumnBuilderRuntimeConfig<any, any>,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class GelTimestampTz<T extends ColumnBaseConfig<'date', 'GelTimestampTz'>> extends GelColumn<T> {
|
||||
static override readonly [entityKind]: string = 'GelTimestampTz';
|
||||
|
||||
constructor(table: AnyGelTable<{ name: T['tableName'] }>, config: GelTimestampTzBuilder<T>['config']) {
|
||||
super(table, config);
|
||||
}
|
||||
|
||||
getSQLType(): string {
|
||||
return 'datetime';
|
||||
}
|
||||
}
|
||||
|
||||
export function timestamptz(): GelTimestampTzBuilderInitial<''>;
|
||||
export function timestamptz<TName extends string>(
|
||||
name: TName,
|
||||
): GelTimestampTzBuilderInitial<TName>;
|
||||
export function timestamptz(name?: string) {
|
||||
return new GelTimestampTzBuilder(name ?? '');
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { ColumnBuilderBaseConfig, ColumnBuilderRuntimeConfig, MakeColumnConfig } from '~/column-builder.ts';
|
||||
import type { ColumnBaseConfig } from '~/column.ts';
|
||||
import { entityKind } from '~/entity.ts';
|
||||
import type { AnyGelTable } from '~/gel-core/table.ts';
|
||||
import { GelColumn, GelColumnBuilder } from './common.ts';
|
||||
|
||||
export type GelUUIDBuilderInitial<TName extends string> = GelUUIDBuilder<{
|
||||
name: TName;
|
||||
dataType: 'string';
|
||||
columnType: 'GelUUID';
|
||||
data: string;
|
||||
driverParam: string;
|
||||
enumValues: undefined;
|
||||
}>;
|
||||
|
||||
export class GelUUIDBuilder<T extends ColumnBuilderBaseConfig<'string', 'GelUUID'>> extends GelColumnBuilder<T> {
|
||||
static override readonly [entityKind]: string = 'GelUUIDBuilder';
|
||||
|
||||
constructor(name: T['name']) {
|
||||
super(name, 'string', 'GelUUID');
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
override build<TTableName extends string>(
|
||||
table: AnyGelTable<{ name: TTableName }>,
|
||||
): GelUUID<MakeColumnConfig<T, TTableName>> {
|
||||
return new GelUUID<MakeColumnConfig<T, TTableName>>(table, this.config as ColumnBuilderRuntimeConfig<any, any>);
|
||||
}
|
||||
}
|
||||
|
||||
export class GelUUID<T extends ColumnBaseConfig<'string', 'GelUUID'>> extends GelColumn<T> {
|
||||
static override readonly [entityKind]: string = 'GelUUID';
|
||||
|
||||
getSQLType(): string {
|
||||
return 'uuid';
|
||||
}
|
||||
}
|
||||
|
||||
export function uuid(): GelUUIDBuilderInitial<''>;
|
||||
export function uuid<TName extends string>(name: TName): GelUUIDBuilderInitial<TName>;
|
||||
export function uuid(name?: string) {
|
||||
return new GelUUIDBuilder(name ?? '');
|
||||
}
|
||||
@@ -0,0 +1,674 @@
|
||||
import type { Cache } from '~/cache/core/cache.ts';
|
||||
import { entityKind } from '~/entity.ts';
|
||||
import type { GelDialect } from '~/gel-core/dialect.ts';
|
||||
import {
|
||||
GelDeleteBase,
|
||||
GelInsertBuilder,
|
||||
GelSelectBuilder,
|
||||
GelUpdateBuilder,
|
||||
QueryBuilder,
|
||||
} from '~/gel-core/query-builders/index.ts';
|
||||
import type { GelQueryResultHKT, GelSession, GelTransaction, PreparedQueryConfig } from '~/gel-core/session.ts';
|
||||
import type { GelTable } from '~/gel-core/table.ts';
|
||||
import type { TypedQueryBuilder } from '~/query-builders/query-builder.ts';
|
||||
import type { ExtractTablesWithRelations, RelationalSchemaConfig, TablesRelationalConfig } from '~/relations.ts';
|
||||
import { SelectionProxyHandler } from '~/selection-proxy.ts';
|
||||
import { type ColumnsSelection, type SQL, sql, type SQLWrapper } from '~/sql/sql.ts';
|
||||
import { WithSubquery } from '~/subquery.ts';
|
||||
import type { DrizzleTypeError } from '~/utils.ts';
|
||||
import type { GelColumn } from './columns/index.ts';
|
||||
import { GelCountBuilder } from './query-builders/count.ts';
|
||||
import { RelationalQueryBuilder } from './query-builders/query.ts';
|
||||
import { GelRaw } from './query-builders/raw.ts';
|
||||
import type { SelectedFields } from './query-builders/select.types.ts';
|
||||
import type { WithSubqueryWithSelection } from './subquery.ts';
|
||||
import type { GelViewBase } from './view-base.ts';
|
||||
|
||||
export class GelDatabase<
|
||||
TQueryResult extends GelQueryResultHKT,
|
||||
TFullSchema extends Record<string, unknown> = Record<string, never>,
|
||||
TSchema extends TablesRelationalConfig = ExtractTablesWithRelations<TFullSchema>,
|
||||
> {
|
||||
static readonly [entityKind]: string = 'GelDatabase';
|
||||
|
||||
declare readonly _: {
|
||||
readonly schema: TSchema | undefined;
|
||||
readonly fullSchema: TFullSchema;
|
||||
readonly tableNamesMap: Record<string, string>;
|
||||
readonly session: GelSession<TQueryResult, TFullSchema, TSchema>;
|
||||
};
|
||||
|
||||
query: TFullSchema extends Record<string, never>
|
||||
? DrizzleTypeError<'Seems like the schema generic is missing - did you forget to add it to your DB type?'>
|
||||
: {
|
||||
[K in keyof TSchema]: RelationalQueryBuilder<TSchema, TSchema[K]>;
|
||||
};
|
||||
|
||||
constructor(
|
||||
/** @internal */
|
||||
readonly dialect: GelDialect,
|
||||
/** @internal */
|
||||
readonly session: GelSession<any, any, any>,
|
||||
schema: RelationalSchemaConfig<TSchema> | undefined,
|
||||
) {
|
||||
this._ = schema
|
||||
? {
|
||||
schema: schema.schema,
|
||||
fullSchema: schema.fullSchema as TFullSchema,
|
||||
tableNamesMap: schema.tableNamesMap,
|
||||
session,
|
||||
}
|
||||
: {
|
||||
schema: undefined,
|
||||
fullSchema: {} as TFullSchema,
|
||||
tableNamesMap: {},
|
||||
session,
|
||||
};
|
||||
this.query = {} as typeof this['query'];
|
||||
if (this._.schema) {
|
||||
for (const [tableName, columns] of Object.entries(this._.schema)) {
|
||||
(this.query as GelDatabase<TQueryResult, Record<string, any>>['query'])[tableName] = new RelationalQueryBuilder(
|
||||
schema!.fullSchema,
|
||||
this._.schema,
|
||||
this._.tableNamesMap,
|
||||
schema!.fullSchema[tableName] as GelTable,
|
||||
columns,
|
||||
dialect,
|
||||
session,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.$cache = { invalidate: async (_params: any) => {} };
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a subquery that defines a temporary named result set as a CTE.
|
||||
*
|
||||
* It is useful for breaking down complex queries into simpler parts and for reusing the result set in subsequent parts of the query.
|
||||
*
|
||||
* See docs: {@link https://orm.drizzle.team/docs/select#with-clause}
|
||||
*
|
||||
* @param alias The alias for the subquery.
|
||||
*
|
||||
* Failure to provide an alias will result in a DrizzleTypeError, preventing the subquery from being referenced in other queries.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* ```ts
|
||||
* // Create a subquery with alias 'sq' and use it in the select query
|
||||
* const sq = db.$with('sq').as(db.select().from(users).where(eq(users.id, 42)));
|
||||
*
|
||||
* const result = await db.with(sq).select().from(sq);
|
||||
* ```
|
||||
*
|
||||
* To select arbitrary SQL values as fields in a CTE and reference them in other CTEs or in the main query, you need to add aliases to them:
|
||||
*
|
||||
* ```ts
|
||||
* // Select an arbitrary SQL value as a field in a CTE and reference it in the main query
|
||||
* const sq = db.$with('sq').as(db.select({
|
||||
* name: sql<string>`upper(${users.name})`.as('name'),
|
||||
* })
|
||||
* .from(users));
|
||||
*
|
||||
* const result = await db.with(sq).select({ name: sq.name }).from(sq);
|
||||
* ```
|
||||
*/
|
||||
$with<TAlias extends string>(alias: TAlias) {
|
||||
const self = this;
|
||||
return {
|
||||
as<TSelection extends ColumnsSelection>(
|
||||
qb: TypedQueryBuilder<TSelection> | ((qb: QueryBuilder) => TypedQueryBuilder<TSelection>),
|
||||
): WithSubqueryWithSelection<TSelection, TAlias> {
|
||||
if (typeof qb === 'function') {
|
||||
qb = qb(new QueryBuilder(self.dialect));
|
||||
}
|
||||
|
||||
return new Proxy(
|
||||
new WithSubquery(qb.getSQL(), qb.getSelectedFields() as SelectedFields, alias, true),
|
||||
new SelectionProxyHandler({ alias, sqlAliasedBehavior: 'alias', sqlBehavior: 'error' }),
|
||||
) as WithSubqueryWithSelection<TSelection, TAlias>;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
$count(
|
||||
source: GelTable | GelViewBase | SQL | SQLWrapper,
|
||||
filters?: SQL<unknown>,
|
||||
) {
|
||||
return new GelCountBuilder({ source, filters, session: this.session });
|
||||
}
|
||||
|
||||
/**
|
||||
* Incorporates a previously defined CTE (using `$with`) into the main query.
|
||||
*
|
||||
* This method allows the main query to reference a temporary named result set.
|
||||
*
|
||||
* See docs: {@link https://orm.drizzle.team/docs/select#with-clause}
|
||||
*
|
||||
* @param queries The CTEs to incorporate into the main query.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* ```ts
|
||||
* // Define a subquery 'sq' as a CTE using $with
|
||||
* const sq = db.$with('sq').as(db.select().from(users).where(eq(users.id, 42)));
|
||||
*
|
||||
* // Incorporate the CTE 'sq' into the main query and select from it
|
||||
* const result = await db.with(sq).select().from(sq);
|
||||
* ```
|
||||
*/
|
||||
with(...queries: WithSubquery[]) {
|
||||
const self = this;
|
||||
|
||||
/**
|
||||
* Creates a select query.
|
||||
*
|
||||
* Calling this method with no arguments will select all columns from the table. Pass a selection object to specify the columns you want to select.
|
||||
*
|
||||
* Use `.from()` method to specify which table to select from.
|
||||
*
|
||||
* See docs: {@link https://orm.drizzle.team/docs/select}
|
||||
*
|
||||
* @param fields The selection object.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* ```ts
|
||||
* // Select all columns and all rows from the 'cars' table
|
||||
* const allCars: Car[] = await db.select().from(cars);
|
||||
*
|
||||
* // Select specific columns and all rows from the 'cars' table
|
||||
* const carsIdsAndBrands: { id: number; brand: string }[] = await db.select({
|
||||
* id: cars.id,
|
||||
* brand: cars.brand
|
||||
* })
|
||||
* .from(cars);
|
||||
* ```
|
||||
*
|
||||
* Like in SQL, you can use arbitrary expressions as selection fields, not just table columns:
|
||||
*
|
||||
* ```ts
|
||||
* // Select specific columns along with expression and all rows from the 'cars' table
|
||||
* const carsIdsAndLowerNames: { id: number; lowerBrand: string }[] = await db.select({
|
||||
* id: cars.id,
|
||||
* lowerBrand: sql<string>`lower(${cars.brand})`,
|
||||
* })
|
||||
* .from(cars);
|
||||
* ```
|
||||
*/
|
||||
function select(): GelSelectBuilder<undefined>;
|
||||
function select<TSelection extends SelectedFields>(fields: TSelection): GelSelectBuilder<TSelection>;
|
||||
function select(fields?: SelectedFields): GelSelectBuilder<SelectedFields | undefined> {
|
||||
return new GelSelectBuilder({
|
||||
fields: fields ?? undefined,
|
||||
session: self.session,
|
||||
dialect: self.dialect,
|
||||
withList: queries,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds `distinct` expression to the select query.
|
||||
*
|
||||
* Calling this method will return only unique values. When multiple columns are selected, it returns rows with unique combinations of values in these columns.
|
||||
*
|
||||
* Use `.from()` method to specify which table to select from.
|
||||
*
|
||||
* See docs: {@link https://orm.drizzle.team/docs/select#distinct}
|
||||
*
|
||||
* @param fields The selection object.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // Select all unique rows from the 'cars' table
|
||||
* await db.selectDistinct()
|
||||
* .from(cars)
|
||||
* .orderBy(cars.id, cars.brand, cars.color);
|
||||
*
|
||||
* // Select all unique brands from the 'cars' table
|
||||
* await db.selectDistinct({ brand: cars.brand })
|
||||
* .from(cars)
|
||||
* .orderBy(cars.brand);
|
||||
* ```
|
||||
*/
|
||||
function selectDistinct(): GelSelectBuilder<undefined>;
|
||||
function selectDistinct<TSelection extends SelectedFields>(fields: TSelection): GelSelectBuilder<TSelection>;
|
||||
function selectDistinct(fields?: SelectedFields): GelSelectBuilder<SelectedFields | undefined> {
|
||||
return new GelSelectBuilder({
|
||||
fields: fields ?? undefined,
|
||||
session: self.session,
|
||||
dialect: self.dialect,
|
||||
withList: queries,
|
||||
distinct: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds `distinct on` expression to the select query.
|
||||
*
|
||||
* Calling this method will specify how the unique rows are determined.
|
||||
*
|
||||
* Use `.from()` method to specify which table to select from.
|
||||
*
|
||||
* See docs: {@link https://orm.drizzle.team/docs/select#distinct}
|
||||
*
|
||||
* @param on The expression defining uniqueness.
|
||||
* @param fields The selection object.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // Select the first row for each unique brand from the 'cars' table
|
||||
* await db.selectDistinctOn([cars.brand])
|
||||
* .from(cars)
|
||||
* .orderBy(cars.brand);
|
||||
*
|
||||
* // Selects the first occurrence of each unique car brand along with its color from the 'cars' table
|
||||
* await db.selectDistinctOn([cars.brand], { brand: cars.brand, color: cars.color })
|
||||
* .from(cars)
|
||||
* .orderBy(cars.brand, cars.color);
|
||||
* ```
|
||||
*/
|
||||
function selectDistinctOn(on: (GelColumn | SQLWrapper)[]): GelSelectBuilder<undefined>;
|
||||
function selectDistinctOn<TSelection extends SelectedFields>(
|
||||
on: (GelColumn | SQLWrapper)[],
|
||||
fields: TSelection,
|
||||
): GelSelectBuilder<TSelection>;
|
||||
function selectDistinctOn(
|
||||
on: (GelColumn | SQLWrapper)[],
|
||||
fields?: SelectedFields,
|
||||
): GelSelectBuilder<SelectedFields | undefined> {
|
||||
return new GelSelectBuilder({
|
||||
fields: fields ?? undefined,
|
||||
session: self.session,
|
||||
dialect: self.dialect,
|
||||
withList: queries,
|
||||
distinct: { on },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an update query.
|
||||
*
|
||||
* Calling this method without `.where()` clause will update all rows in a table. The `.where()` clause specifies which rows should be updated.
|
||||
*
|
||||
* Use `.set()` method to specify which values to update.
|
||||
*
|
||||
* See docs: {@link https://orm.drizzle.team/docs/update}
|
||||
*
|
||||
* @param table The table to update.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* ```ts
|
||||
* // Update all rows in the 'cars' table
|
||||
* await db.update(cars).set({ color: 'red' });
|
||||
*
|
||||
* // Update rows with filters and conditions
|
||||
* await db.update(cars).set({ color: 'red' }).where(eq(cars.brand, 'BMW'));
|
||||
*
|
||||
* // Update with returning clause
|
||||
* const updatedCar: Car[] = await db.update(cars)
|
||||
* .set({ color: 'red' })
|
||||
* .where(eq(cars.id, 1))
|
||||
* .returning();
|
||||
* ```
|
||||
*/
|
||||
function update<TTable extends GelTable>(table: TTable): GelUpdateBuilder<TTable, TQueryResult> {
|
||||
return new GelUpdateBuilder(table, self.session, self.dialect, queries);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an insert query.
|
||||
*
|
||||
* Calling this method will create new rows in a table. Use `.values()` method to specify which values to insert.
|
||||
*
|
||||
* See docs: {@link https://orm.drizzle.team/docs/insert}
|
||||
*
|
||||
* @param table The table to insert into.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* ```ts
|
||||
* // Insert one row
|
||||
* await db.insert(cars).values({ brand: 'BMW' });
|
||||
*
|
||||
* // Insert multiple rows
|
||||
* await db.insert(cars).values([{ brand: 'BMW' }, { brand: 'Porsche' }]);
|
||||
*
|
||||
* // Insert with returning clause
|
||||
* const insertedCar: Car[] = await db.insert(cars)
|
||||
* .values({ brand: 'BMW' })
|
||||
* .returning();
|
||||
* ```
|
||||
*/
|
||||
function insert<TTable extends GelTable>(table: TTable): GelInsertBuilder<TTable, TQueryResult> {
|
||||
return new GelInsertBuilder(table, self.session, self.dialect, queries);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a delete query.
|
||||
*
|
||||
* Calling this method without `.where()` clause will delete all rows in a table. The `.where()` clause specifies which rows should be deleted.
|
||||
*
|
||||
* See docs: {@link https://orm.drizzle.team/docs/delete}
|
||||
*
|
||||
* @param table The table to delete from.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* ```ts
|
||||
* // Delete all rows in the 'cars' table
|
||||
* await db.delete(cars);
|
||||
*
|
||||
* // Delete rows with filters and conditions
|
||||
* await db.delete(cars).where(eq(cars.color, 'green'));
|
||||
*
|
||||
* // Delete with returning clause
|
||||
* const deletedCar: Car[] = await db.delete(cars)
|
||||
* .where(eq(cars.id, 1))
|
||||
* .returning();
|
||||
* ```
|
||||
*/
|
||||
function delete_<TTable extends GelTable>(table: TTable): GelDeleteBase<TTable, TQueryResult> {
|
||||
return new GelDeleteBase(table, self.session, self.dialect, queries);
|
||||
}
|
||||
|
||||
return { select, selectDistinct, selectDistinctOn, update, insert, delete: delete_ };
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a select query.
|
||||
*
|
||||
* Calling this method with no arguments will select all columns from the table. Pass a selection object to specify the columns you want to select.
|
||||
*
|
||||
* Use `.from()` method to specify which table to select from.
|
||||
*
|
||||
* See docs: {@link https://orm.drizzle.team/docs/select}
|
||||
*
|
||||
* @param fields The selection object.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* ```ts
|
||||
* // Select all columns and all rows from the 'cars' table
|
||||
* const allCars: Car[] = await db.select().from(cars);
|
||||
*
|
||||
* // Select specific columns and all rows from the 'cars' table
|
||||
* const carsIdsAndBrands: { id: number; brand: string }[] = await db.select({
|
||||
* id: cars.id,
|
||||
* brand: cars.brand
|
||||
* })
|
||||
* .from(cars);
|
||||
* ```
|
||||
*
|
||||
* Like in SQL, you can use arbitrary expressions as selection fields, not just table columns:
|
||||
*
|
||||
* ```ts
|
||||
* // Select specific columns along with expression and all rows from the 'cars' table
|
||||
* const carsIdsAndLowerNames: { id: number; lowerBrand: string }[] = await db.select({
|
||||
* id: cars.id,
|
||||
* lowerBrand: sql<string>`lower(${cars.brand})`,
|
||||
* })
|
||||
* .from(cars);
|
||||
* ```
|
||||
*/
|
||||
select(): GelSelectBuilder<undefined>;
|
||||
select<TSelection extends SelectedFields>(fields: TSelection): GelSelectBuilder<TSelection>;
|
||||
select(fields?: SelectedFields): GelSelectBuilder<SelectedFields | undefined> {
|
||||
return new GelSelectBuilder({
|
||||
fields: fields ?? undefined,
|
||||
session: this.session,
|
||||
dialect: this.dialect,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds `distinct` expression to the select query.
|
||||
*
|
||||
* Calling this method will return only unique values. When multiple columns are selected, it returns rows with unique combinations of values in these columns.
|
||||
*
|
||||
* Use `.from()` method to specify which table to select from.
|
||||
*
|
||||
* See docs: {@link https://orm.drizzle.team/docs/select#distinct}
|
||||
*
|
||||
* @param fields The selection object.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // Select all unique rows from the 'cars' table
|
||||
* await db.selectDistinct()
|
||||
* .from(cars)
|
||||
* .orderBy(cars.id, cars.brand, cars.color);
|
||||
*
|
||||
* // Select all unique brands from the 'cars' table
|
||||
* await db.selectDistinct({ brand: cars.brand })
|
||||
* .from(cars)
|
||||
* .orderBy(cars.brand);
|
||||
* ```
|
||||
*/
|
||||
selectDistinct(): GelSelectBuilder<undefined>;
|
||||
selectDistinct<TSelection extends SelectedFields>(fields: TSelection): GelSelectBuilder<TSelection>;
|
||||
selectDistinct(fields?: SelectedFields): GelSelectBuilder<SelectedFields | undefined> {
|
||||
return new GelSelectBuilder({
|
||||
fields: fields ?? undefined,
|
||||
session: this.session,
|
||||
dialect: this.dialect,
|
||||
distinct: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds `distinct on` expression to the select query.
|
||||
*
|
||||
* Calling this method will specify how the unique rows are determined.
|
||||
*
|
||||
* Use `.from()` method to specify which table to select from.
|
||||
*
|
||||
* See docs: {@link https://orm.drizzle.team/docs/select#distinct}
|
||||
*
|
||||
* @param on The expression defining uniqueness.
|
||||
* @param fields The selection object.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // Select the first row for each unique brand from the 'cars' table
|
||||
* await db.selectDistinctOn([cars.brand])
|
||||
* .from(cars)
|
||||
* .orderBy(cars.brand);
|
||||
*
|
||||
* // Selects the first occurrence of each unique car brand along with its color from the 'cars' table
|
||||
* await db.selectDistinctOn([cars.brand], { brand: cars.brand, color: cars.color })
|
||||
* .from(cars)
|
||||
* .orderBy(cars.brand, cars.color);
|
||||
* ```
|
||||
*/
|
||||
selectDistinctOn(on: (GelColumn | SQLWrapper)[]): GelSelectBuilder<undefined>;
|
||||
selectDistinctOn<TSelection extends SelectedFields>(
|
||||
on: (GelColumn | SQLWrapper)[],
|
||||
fields: TSelection,
|
||||
): GelSelectBuilder<TSelection>;
|
||||
selectDistinctOn(
|
||||
on: (GelColumn | SQLWrapper)[],
|
||||
fields?: SelectedFields,
|
||||
): GelSelectBuilder<SelectedFields | undefined> {
|
||||
return new GelSelectBuilder({
|
||||
fields: fields ?? undefined,
|
||||
session: this.session,
|
||||
dialect: this.dialect,
|
||||
distinct: { on },
|
||||
});
|
||||
}
|
||||
|
||||
$cache: { invalidate: Cache['onMutate'] };
|
||||
|
||||
/**
|
||||
* Creates an update query.
|
||||
*
|
||||
* Calling this method without `.where()` clause will update all rows in a table. The `.where()` clause specifies which rows should be updated.
|
||||
*
|
||||
* Use `.set()` method to specify which values to update.
|
||||
*
|
||||
* See docs: {@link https://orm.drizzle.team/docs/update}
|
||||
*
|
||||
* @param table The table to update.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* ```ts
|
||||
* // Update all rows in the 'cars' table
|
||||
* await db.update(cars).set({ color: 'red' });
|
||||
*
|
||||
* // Update rows with filters and conditions
|
||||
* await db.update(cars).set({ color: 'red' }).where(eq(cars.brand, 'BMW'));
|
||||
*
|
||||
* // Update with returning clause
|
||||
* const updatedCar: Car[] = await db.update(cars)
|
||||
* .set({ color: 'red' })
|
||||
* .where(eq(cars.id, 1))
|
||||
* .returning();
|
||||
* ```
|
||||
*/
|
||||
update<TTable extends GelTable>(table: TTable): GelUpdateBuilder<TTable, TQueryResult> {
|
||||
return new GelUpdateBuilder(table, this.session, this.dialect);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an insert query.
|
||||
*
|
||||
* Calling this method will create new rows in a table. Use `.values()` method to specify which values to insert.
|
||||
*
|
||||
* See docs: {@link https://orm.drizzle.team/docs/insert}
|
||||
*
|
||||
* @param table The table to insert into.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* ```ts
|
||||
* // Insert one row
|
||||
* await db.insert(cars).values({ brand: 'BMW' });
|
||||
*
|
||||
* // Insert multiple rows
|
||||
* await db.insert(cars).values([{ brand: 'BMW' }, { brand: 'Porsche' }]);
|
||||
*
|
||||
* // Insert with returning clause
|
||||
* const insertedCar: Car[] = await db.insert(cars)
|
||||
* .values({ brand: 'BMW' })
|
||||
* .returning();
|
||||
* ```
|
||||
*/
|
||||
insert<TTable extends GelTable>(table: TTable): GelInsertBuilder<TTable, TQueryResult> {
|
||||
return new GelInsertBuilder(table, this.session, this.dialect);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a delete query.
|
||||
*
|
||||
* Calling this method without `.where()` clause will delete all rows in a table. The `.where()` clause specifies which rows should be deleted.
|
||||
*
|
||||
* See docs: {@link https://orm.drizzle.team/docs/delete}
|
||||
*
|
||||
* @param table The table to delete from.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* ```ts
|
||||
* // Delete all rows in the 'cars' table
|
||||
* await db.delete(cars);
|
||||
*
|
||||
* // Delete rows with filters and conditions
|
||||
* await db.delete(cars).where(eq(cars.color, 'green'));
|
||||
*
|
||||
* // Delete with returning clause
|
||||
* const deletedCar: Car[] = await db.delete(cars)
|
||||
* .where(eq(cars.id, 1))
|
||||
* .returning();
|
||||
* ```
|
||||
*/
|
||||
delete<TTable extends GelTable>(table: TTable): GelDeleteBase<TTable, TQueryResult> {
|
||||
return new GelDeleteBase(table, this.session, this.dialect);
|
||||
}
|
||||
|
||||
// TODO views are not implemented
|
||||
// refreshMaterializedView<TView extends GelMaterializedView>(view: TView): GelRefreshMaterializedView<TQueryResult> {
|
||||
// return new GelRefreshMaterializedView(view, this.session, this.dialect);
|
||||
// }
|
||||
|
||||
execute<TRow extends Record<string, unknown> = Record<string, unknown>>(
|
||||
query: SQLWrapper | string,
|
||||
): GelRaw<TRow[]> {
|
||||
const sequel = typeof query === 'string' ? sql.raw(query) : query.getSQL();
|
||||
const builtQuery = this.dialect.sqlToQuery(sequel);
|
||||
const prepared = this.session.prepareQuery<
|
||||
PreparedQueryConfig & { execute: TRow[] }
|
||||
>(
|
||||
builtQuery,
|
||||
undefined,
|
||||
undefined,
|
||||
false,
|
||||
);
|
||||
return new GelRaw(
|
||||
() => prepared.execute(undefined),
|
||||
sequel,
|
||||
builtQuery,
|
||||
(result) => prepared.mapResult(result, true),
|
||||
);
|
||||
}
|
||||
|
||||
transaction<T>(
|
||||
transaction: (tx: GelTransaction<TQueryResult, TFullSchema, TSchema>) => Promise<T>,
|
||||
): Promise<T> {
|
||||
return this.session.transaction(transaction);
|
||||
}
|
||||
}
|
||||
|
||||
export type GelWithReplicas<Q> = Q & { $primary: Q; $replicas: Q[] };
|
||||
|
||||
export const withReplicas = <
|
||||
HKT extends GelQueryResultHKT,
|
||||
TFullSchema extends Record<string, unknown>,
|
||||
TSchema extends TablesRelationalConfig,
|
||||
Q extends GelDatabase<
|
||||
HKT,
|
||||
TFullSchema,
|
||||
TSchema extends Record<string, unknown> ? ExtractTablesWithRelations<TFullSchema> : TSchema
|
||||
>,
|
||||
>(
|
||||
primary: Q,
|
||||
replicas: [Q, ...Q[]],
|
||||
getReplica: (replicas: Q[]) => Q = () => replicas[Math.floor(Math.random() * replicas.length)]!,
|
||||
): GelWithReplicas<Q> => {
|
||||
const select: Q['select'] = (...args: []) => getReplica(replicas).select(...args);
|
||||
const selectDistinct: Q['selectDistinct'] = (...args: []) => getReplica(replicas).selectDistinct(...args);
|
||||
const selectDistinctOn: Q['selectDistinctOn'] = (...args: [any]) => getReplica(replicas).selectDistinctOn(...args);
|
||||
const _with: Q['with'] = (...args: any) => getReplica(replicas).with(...args);
|
||||
const $with: Q['$with'] = (arg: any) => getReplica(replicas).$with(arg);
|
||||
|
||||
const update: Q['update'] = (...args: [any]) => primary.update(...args);
|
||||
const insert: Q['insert'] = (...args: [any]) => primary.insert(...args);
|
||||
const $delete: Q['delete'] = (...args: [any]) => primary.delete(...args);
|
||||
const execute: Q['execute'] = (...args: [any]) => primary.execute(...args);
|
||||
const transaction: Q['transaction'] = (...args: [any]) => primary.transaction(...args);
|
||||
// const refreshMaterializedView: Q['refreshMaterializedView'] = (...args: [any]) =>
|
||||
// primary.refreshMaterializedView(...args);
|
||||
|
||||
return {
|
||||
...primary,
|
||||
update,
|
||||
insert,
|
||||
delete: $delete,
|
||||
execute,
|
||||
transaction,
|
||||
// refreshMaterializedView,
|
||||
$primary: primary,
|
||||
$replicas: replicas,
|
||||
select,
|
||||
selectDistinct,
|
||||
selectDistinctOn,
|
||||
$with,
|
||||
with: _with,
|
||||
get query() {
|
||||
return getReplica(replicas).query;
|
||||
},
|
||||
};
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,25 @@
|
||||
import type { GelColumn } from '~/gel-core/columns/index.ts';
|
||||
import { bindIfParam } from '~/sql/expressions/index.ts';
|
||||
import type { Placeholder, SQL, SQLChunk, SQLWrapper } from '~/sql/sql.ts';
|
||||
import { sql } from '~/sql/sql.ts';
|
||||
|
||||
export * from '~/sql/expressions/index.ts';
|
||||
|
||||
export function concat(column: GelColumn | SQL.Aliased, value: string | Placeholder | SQLWrapper): SQL {
|
||||
return sql`${column} || ${bindIfParam(value, column)}`;
|
||||
}
|
||||
|
||||
export function substring(
|
||||
column: GelColumn | SQL.Aliased,
|
||||
{ from, for: _for }: { from?: number | Placeholder | SQLWrapper; for?: number | Placeholder | SQLWrapper },
|
||||
): SQL {
|
||||
const chunks: SQLChunk[] = [sql`substring(`, column];
|
||||
if (from !== undefined) {
|
||||
chunks.push(sql` from `, bindIfParam(from, column));
|
||||
}
|
||||
if (_for !== undefined) {
|
||||
chunks.push(sql` for `, bindIfParam(_for, column));
|
||||
}
|
||||
chunks.push(sql`)`);
|
||||
return sql.join(chunks);
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import { entityKind } from '~/entity.ts';
|
||||
import { TableName } from '~/table.utils.ts';
|
||||
import type { AnyGelColumn, GelColumn } from './columns/index.ts';
|
||||
import type { GelTable } from './table.ts';
|
||||
|
||||
export type UpdateDeleteAction = 'cascade' | 'restrict' | 'no action' | 'set null' | 'set default';
|
||||
|
||||
export type Reference = () => {
|
||||
readonly name?: string;
|
||||
readonly columns: GelColumn[];
|
||||
readonly foreignTable: GelTable;
|
||||
readonly foreignColumns: GelColumn[];
|
||||
};
|
||||
|
||||
export class ForeignKeyBuilder {
|
||||
static readonly [entityKind]: string = 'GelForeignKeyBuilder';
|
||||
|
||||
/** @internal */
|
||||
reference: Reference;
|
||||
|
||||
/** @internal */
|
||||
_onUpdate: UpdateDeleteAction | undefined = 'no action';
|
||||
|
||||
/** @internal */
|
||||
_onDelete: UpdateDeleteAction | undefined = 'no action';
|
||||
|
||||
constructor(
|
||||
config: () => {
|
||||
name?: string;
|
||||
columns: GelColumn[];
|
||||
foreignColumns: GelColumn[];
|
||||
},
|
||||
actions?: {
|
||||
onUpdate?: UpdateDeleteAction;
|
||||
onDelete?: UpdateDeleteAction;
|
||||
} | undefined,
|
||||
) {
|
||||
this.reference = () => {
|
||||
const { name, columns, foreignColumns } = config();
|
||||
return { name, columns, foreignTable: foreignColumns[0]!.table as GelTable, foreignColumns };
|
||||
};
|
||||
if (actions) {
|
||||
this._onUpdate = actions.onUpdate;
|
||||
this._onDelete = actions.onDelete;
|
||||
}
|
||||
}
|
||||
|
||||
onUpdate(action: UpdateDeleteAction): this {
|
||||
this._onUpdate = action === undefined ? 'no action' : action;
|
||||
return this;
|
||||
}
|
||||
|
||||
onDelete(action: UpdateDeleteAction): this {
|
||||
this._onDelete = action === undefined ? 'no action' : action;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
build(table: GelTable): ForeignKey {
|
||||
return new ForeignKey(table, this);
|
||||
}
|
||||
}
|
||||
|
||||
export type AnyForeignKeyBuilder = ForeignKeyBuilder;
|
||||
|
||||
export class ForeignKey {
|
||||
static readonly [entityKind]: string = 'GelForeignKey';
|
||||
|
||||
readonly reference: Reference;
|
||||
readonly onUpdate: UpdateDeleteAction | undefined;
|
||||
readonly onDelete: UpdateDeleteAction | undefined;
|
||||
|
||||
constructor(readonly table: GelTable, builder: ForeignKeyBuilder) {
|
||||
this.reference = builder.reference;
|
||||
this.onUpdate = builder._onUpdate;
|
||||
this.onDelete = builder._onDelete;
|
||||
}
|
||||
|
||||
getName(): string {
|
||||
const { name, columns, foreignColumns } = this.reference();
|
||||
const columnNames = columns.map((column) => column.name);
|
||||
const foreignColumnNames = foreignColumns.map((column) => column.name);
|
||||
const chunks = [
|
||||
this.table[TableName],
|
||||
...columnNames,
|
||||
foreignColumns[0]!.table[TableName],
|
||||
...foreignColumnNames,
|
||||
];
|
||||
return name ?? `${chunks.join('_')}_fk`;
|
||||
}
|
||||
}
|
||||
|
||||
type ColumnsWithTable<
|
||||
TTableName extends string,
|
||||
TColumns extends GelColumn[],
|
||||
> = { [Key in keyof TColumns]: AnyGelColumn<{ tableName: TTableName }> };
|
||||
|
||||
export function foreignKey<
|
||||
TTableName extends string,
|
||||
TForeignTableName extends string,
|
||||
TColumns extends [AnyGelColumn<{ tableName: TTableName }>, ...AnyGelColumn<{ tableName: TTableName }>[]],
|
||||
>(
|
||||
config: {
|
||||
name?: string;
|
||||
columns: TColumns;
|
||||
foreignColumns: ColumnsWithTable<TForeignTableName, TColumns>;
|
||||
},
|
||||
): ForeignKeyBuilder {
|
||||
function mappedConfig() {
|
||||
const { name, columns, foreignColumns } = config;
|
||||
return {
|
||||
name,
|
||||
columns,
|
||||
foreignColumns,
|
||||
};
|
||||
}
|
||||
|
||||
return new ForeignKeyBuilder(mappedConfig);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
export * from './alias.ts';
|
||||
export * from './checks.ts';
|
||||
export * from './columns/index.ts';
|
||||
export * from './db.ts';
|
||||
export * from './dialect.ts';
|
||||
export * from './foreign-keys.ts';
|
||||
export * from './indexes.ts';
|
||||
export * from './policies.ts';
|
||||
export * from './primary-keys.ts';
|
||||
export * from './query-builders/index.ts';
|
||||
export * from './roles.ts';
|
||||
export * from './schema.ts';
|
||||
export * from './sequence.ts';
|
||||
export * from './session.ts';
|
||||
export * from './subquery.ts';
|
||||
export * from './table.ts';
|
||||
export * from './unique-constraint.ts';
|
||||
export * from './utils.ts';
|
||||
export * from './view-common.ts';
|
||||
export * from './view.ts';
|
||||
@@ -0,0 +1,259 @@
|
||||
import { SQL } from '~/sql/sql.ts';
|
||||
|
||||
import { entityKind, is } from '~/entity.ts';
|
||||
import type { GelColumn, GelExtraConfigColumn } from './columns/index.ts';
|
||||
import { IndexedColumn } from './columns/index.ts';
|
||||
import type { GelTable } from './table.ts';
|
||||
|
||||
interface IndexConfig {
|
||||
name?: string;
|
||||
|
||||
columns: Partial<IndexedColumn | SQL>[];
|
||||
|
||||
/**
|
||||
* If true, the index will be created as `create unique index` instead of `create index`.
|
||||
*/
|
||||
unique: boolean;
|
||||
|
||||
/**
|
||||
* If true, the index will be created as `create index concurrently` instead of `create index`.
|
||||
*/
|
||||
concurrently?: boolean;
|
||||
|
||||
/**
|
||||
* If true, the index will be created as `create index ... on only <table>` instead of `create index ... on <table>`.
|
||||
*/
|
||||
only: boolean;
|
||||
|
||||
/**
|
||||
* Condition for partial index.
|
||||
*/
|
||||
where?: SQL;
|
||||
|
||||
/**
|
||||
* The optional WITH clause specifies storage parameters for the index
|
||||
*/
|
||||
with?: Record<string, any>;
|
||||
|
||||
/**
|
||||
* The optional WITH clause method for the index
|
||||
*/
|
||||
method?: 'btree' | string;
|
||||
}
|
||||
|
||||
export type IndexColumn = GelColumn;
|
||||
|
||||
export type GelIndexMethod =
|
||||
| 'btree'
|
||||
| 'hash'
|
||||
| 'gist'
|
||||
| 'sGelist'
|
||||
| 'gin'
|
||||
| 'brin'
|
||||
| 'hnsw'
|
||||
| 'ivfflat'
|
||||
| (string & {});
|
||||
|
||||
export type GelIndexOpClass =
|
||||
| 'abstime_ops'
|
||||
| 'access_method'
|
||||
| 'anyarray_eq'
|
||||
| 'anyarray_ge'
|
||||
| 'anyarray_gt'
|
||||
| 'anyarray_le'
|
||||
| 'anyarray_lt'
|
||||
| 'anyarray_ne'
|
||||
| 'bigint_ops'
|
||||
| 'bit_ops'
|
||||
| 'bool_ops'
|
||||
| 'box_ops'
|
||||
| 'bpchar_ops'
|
||||
| 'char_ops'
|
||||
| 'cidr_ops'
|
||||
| 'cstring_ops'
|
||||
| 'date_ops'
|
||||
| 'float_ops'
|
||||
| 'int2_ops'
|
||||
| 'int4_ops'
|
||||
| 'int8_ops'
|
||||
| 'interval_ops'
|
||||
| 'jsonb_ops'
|
||||
| 'macaddr_ops'
|
||||
| 'name_ops'
|
||||
| 'numeric_ops'
|
||||
| 'oid_ops'
|
||||
| 'oidint4_ops'
|
||||
| 'oidint8_ops'
|
||||
| 'oidname_ops'
|
||||
| 'oidvector_ops'
|
||||
| 'point_ops'
|
||||
| 'polygon_ops'
|
||||
| 'range_ops'
|
||||
| 'record_eq'
|
||||
| 'record_ge'
|
||||
| 'record_gt'
|
||||
| 'record_le'
|
||||
| 'record_lt'
|
||||
| 'record_ne'
|
||||
| 'text_ops'
|
||||
| 'time_ops'
|
||||
| 'timestamp_ops'
|
||||
| 'timestamptz_ops'
|
||||
| 'timetz_ops'
|
||||
| 'uuid_ops'
|
||||
| 'varbit_ops'
|
||||
| 'varchar_ops'
|
||||
| 'xml_ops'
|
||||
| 'vector_l2_ops'
|
||||
| 'vector_ip_ops'
|
||||
| 'vector_cosine_ops'
|
||||
| 'vector_l1_ops'
|
||||
| 'bit_hamming_ops'
|
||||
| 'bit_jaccard_ops'
|
||||
| 'halfvec_l2_ops'
|
||||
| 'sparsevec_l2_op'
|
||||
| (string & {});
|
||||
|
||||
export class IndexBuilderOn {
|
||||
static readonly [entityKind]: string = 'GelIndexBuilderOn';
|
||||
|
||||
constructor(private unique: boolean, private name?: string) {}
|
||||
|
||||
on(...columns: [Partial<GelExtraConfigColumn> | SQL, ...Partial<GelExtraConfigColumn | SQL>[]]): IndexBuilder {
|
||||
return new IndexBuilder(
|
||||
columns.map((it) => {
|
||||
if (is(it, SQL)) {
|
||||
return it;
|
||||
}
|
||||
it = it as GelExtraConfigColumn;
|
||||
const clonedIndexedColumn = new IndexedColumn(it.name, !!it.keyAsName, it.columnType!, it.indexConfig!);
|
||||
it.indexConfig = JSON.parse(JSON.stringify(it.defaultConfig));
|
||||
return clonedIndexedColumn;
|
||||
}),
|
||||
this.unique,
|
||||
false,
|
||||
this.name,
|
||||
);
|
||||
}
|
||||
|
||||
onOnly(...columns: [Partial<GelExtraConfigColumn | SQL>, ...Partial<GelExtraConfigColumn | SQL>[]]): IndexBuilder {
|
||||
return new IndexBuilder(
|
||||
columns.map((it) => {
|
||||
if (is(it, SQL)) {
|
||||
return it;
|
||||
}
|
||||
it = it as GelExtraConfigColumn;
|
||||
const clonedIndexedColumn = new IndexedColumn(it.name, !!it.keyAsName, it.columnType!, it.indexConfig!);
|
||||
it.indexConfig = it.defaultConfig;
|
||||
return clonedIndexedColumn;
|
||||
}),
|
||||
this.unique,
|
||||
true,
|
||||
this.name,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify what index method to use. Choices are `btree`, `hash`, `gist`, `sGelist`, `gin`, `brin`, or user-installed access methods like `bloom`. The default method is `btree.
|
||||
*
|
||||
* If you have the `Gel_vector` extension installed in your database, you can use the `hnsw` and `ivfflat` options, which are predefined types.
|
||||
*
|
||||
* **You can always specify any string you want in the method, in case Drizzle doesn't have it natively in its types**
|
||||
*
|
||||
* @param method The name of the index method to be used
|
||||
* @param columns
|
||||
* @returns
|
||||
*/
|
||||
using(
|
||||
method: GelIndexMethod,
|
||||
...columns: [Partial<GelExtraConfigColumn | SQL>, ...Partial<GelExtraConfigColumn | SQL>[]]
|
||||
): IndexBuilder {
|
||||
return new IndexBuilder(
|
||||
columns.map((it) => {
|
||||
if (is(it, SQL)) {
|
||||
return it;
|
||||
}
|
||||
it = it as GelExtraConfigColumn;
|
||||
const clonedIndexedColumn = new IndexedColumn(it.name, !!it.keyAsName, it.columnType!, it.indexConfig!);
|
||||
it.indexConfig = JSON.parse(JSON.stringify(it.defaultConfig));
|
||||
return clonedIndexedColumn;
|
||||
}),
|
||||
this.unique,
|
||||
true,
|
||||
this.name,
|
||||
method,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export interface AnyIndexBuilder {
|
||||
build(table: GelTable): Index;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface IndexBuilder extends AnyIndexBuilder {}
|
||||
|
||||
export class IndexBuilder implements AnyIndexBuilder {
|
||||
static readonly [entityKind]: string = 'GelIndexBuilder';
|
||||
|
||||
/** @internal */
|
||||
config: IndexConfig;
|
||||
|
||||
constructor(
|
||||
columns: Partial<IndexedColumn | SQL>[],
|
||||
unique: boolean,
|
||||
only: boolean,
|
||||
name?: string,
|
||||
method: string = 'btree',
|
||||
) {
|
||||
this.config = {
|
||||
name,
|
||||
columns,
|
||||
unique,
|
||||
only,
|
||||
method,
|
||||
};
|
||||
}
|
||||
|
||||
concurrently(): this {
|
||||
this.config.concurrently = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
with(obj: Record<string, any>): this {
|
||||
this.config.with = obj;
|
||||
return this;
|
||||
}
|
||||
|
||||
where(condition: SQL): this {
|
||||
this.config.where = condition;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
build(table: GelTable): Index {
|
||||
return new Index(this.config, table);
|
||||
}
|
||||
}
|
||||
|
||||
export class Index {
|
||||
static readonly [entityKind]: string = 'GelIndex';
|
||||
|
||||
readonly config: IndexConfig & { table: GelTable };
|
||||
|
||||
constructor(config: IndexConfig, table: GelTable) {
|
||||
this.config = { ...config, table };
|
||||
}
|
||||
}
|
||||
|
||||
export type GetColumnsTableName<TColumns> = TColumns extends GelColumn ? TColumns['_']['name']
|
||||
: TColumns extends GelColumn[] ? TColumns[number]['_']['name']
|
||||
: never;
|
||||
|
||||
export function index(name?: string): IndexBuilderOn {
|
||||
return new IndexBuilderOn(false, name);
|
||||
}
|
||||
|
||||
export function uniqueIndex(name?: string): IndexBuilderOn {
|
||||
return new IndexBuilderOn(true, name);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { entityKind } from '~/entity.ts';
|
||||
import type { SQL } from '~/sql/sql.ts';
|
||||
import type { GelRole } from './roles.ts';
|
||||
import type { GelTable } from './table.ts';
|
||||
|
||||
export type GelPolicyToOption =
|
||||
| 'public'
|
||||
| 'current_role'
|
||||
| 'current_user'
|
||||
| 'session_user'
|
||||
| (string & {})
|
||||
| GelPolicyToOption[]
|
||||
| GelRole;
|
||||
|
||||
export interface GelPolicyConfig {
|
||||
as?: 'permissive' | 'restrictive';
|
||||
for?: 'all' | 'select' | 'insert' | 'update' | 'delete';
|
||||
to?: GelPolicyToOption;
|
||||
using?: SQL;
|
||||
withCheck?: SQL;
|
||||
}
|
||||
|
||||
export class GelPolicy implements GelPolicyConfig {
|
||||
static readonly [entityKind]: string = 'GelPolicy';
|
||||
|
||||
readonly as: GelPolicyConfig['as'];
|
||||
readonly for: GelPolicyConfig['for'];
|
||||
readonly to: GelPolicyConfig['to'];
|
||||
readonly using: GelPolicyConfig['using'];
|
||||
readonly withCheck: GelPolicyConfig['withCheck'];
|
||||
|
||||
/** @internal */
|
||||
_linkedTable?: GelTable;
|
||||
|
||||
constructor(
|
||||
readonly name: string,
|
||||
config?: GelPolicyConfig,
|
||||
) {
|
||||
if (config) {
|
||||
this.as = config.as;
|
||||
this.for = config.for;
|
||||
this.to = config.to;
|
||||
this.using = config.using;
|
||||
this.withCheck = config.withCheck;
|
||||
}
|
||||
}
|
||||
|
||||
link(table: GelTable): this {
|
||||
this._linkedTable = table;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
export function gelPolicy(name: string, config?: GelPolicyConfig) {
|
||||
return new GelPolicy(name, config);
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import { entityKind } from '~/entity.ts';
|
||||
import type { AnyGelColumn, GelColumn } from './columns/index.ts';
|
||||
import { GelTable } from './table.ts';
|
||||
|
||||
export function primaryKey<
|
||||
TTableName extends string,
|
||||
TColumn extends AnyGelColumn<{ tableName: TTableName }>,
|
||||
TColumns extends AnyGelColumn<{ tableName: TTableName }>[],
|
||||
>(config: { name?: string; columns: [TColumn, ...TColumns] }): PrimaryKeyBuilder;
|
||||
/**
|
||||
* @deprecated: Please use primaryKey({ columns: [] }) instead of this function
|
||||
* @param columns
|
||||
*/
|
||||
export function primaryKey<
|
||||
TTableName extends string,
|
||||
TColumns extends AnyGelColumn<{ tableName: TTableName }>[],
|
||||
>(...columns: TColumns): PrimaryKeyBuilder;
|
||||
export function primaryKey(...config: any) {
|
||||
if (config[0].columns) {
|
||||
return new PrimaryKeyBuilder(config[0].columns, config[0].name);
|
||||
}
|
||||
return new PrimaryKeyBuilder(config);
|
||||
}
|
||||
|
||||
export class PrimaryKeyBuilder {
|
||||
static readonly [entityKind]: string = 'GelPrimaryKeyBuilder';
|
||||
|
||||
/** @internal */
|
||||
columns: GelColumn[];
|
||||
|
||||
/** @internal */
|
||||
name?: string;
|
||||
|
||||
constructor(
|
||||
columns: GelColumn[],
|
||||
name?: string,
|
||||
) {
|
||||
this.columns = columns;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
build(table: GelTable): PrimaryKey {
|
||||
return new PrimaryKey(table, this.columns, this.name);
|
||||
}
|
||||
}
|
||||
|
||||
export class PrimaryKey {
|
||||
static readonly [entityKind]: string = 'GelPrimaryKey';
|
||||
|
||||
readonly columns: AnyGelColumn<{}>[];
|
||||
readonly name?: string;
|
||||
|
||||
constructor(readonly table: GelTable, columns: AnyGelColumn<{}>[], name?: string) {
|
||||
this.columns = columns;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
getName(): string {
|
||||
return this.name ?? `${this.table[GelTable.Symbol.Name]}_${this.columns.map((column) => column.name).join('_')}_pk`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import { entityKind } from '~/entity.ts';
|
||||
import { SQL, sql, type SQLWrapper } from '~/sql/sql.ts';
|
||||
import type { GelSession } from '../session.ts';
|
||||
import type { GelTable } from '../table.ts';
|
||||
|
||||
export class GelCountBuilder<
|
||||
TSession extends GelSession<any, any, any>,
|
||||
> extends SQL<number> implements Promise<number>, SQLWrapper {
|
||||
private sql: SQL<number>;
|
||||
|
||||
static override readonly [entityKind] = 'GelCountBuilder';
|
||||
[Symbol.toStringTag] = 'GelCountBuilder';
|
||||
|
||||
private session: TSession;
|
||||
|
||||
private static buildEmbeddedCount(
|
||||
source: GelTable | SQL | SQLWrapper,
|
||||
filters?: SQL<unknown>,
|
||||
): SQL<number> {
|
||||
return sql<number>`(select count(*) from ${source}${sql.raw(' where ').if(filters)}${filters})`;
|
||||
}
|
||||
|
||||
private static buildCount(
|
||||
source: GelTable | SQL | SQLWrapper,
|
||||
filters?: SQL<unknown>,
|
||||
): SQL<number> {
|
||||
return sql<number>`select count(*) as count from ${source}${sql.raw(' where ').if(filters)}${filters};`;
|
||||
}
|
||||
|
||||
constructor(
|
||||
readonly params: {
|
||||
source: GelTable | SQL | SQLWrapper;
|
||||
filters?: SQL<unknown>;
|
||||
session: TSession;
|
||||
},
|
||||
) {
|
||||
super(GelCountBuilder.buildEmbeddedCount(params.source, params.filters).queryChunks);
|
||||
|
||||
this.mapWith(Number);
|
||||
|
||||
this.session = params.session;
|
||||
|
||||
this.sql = GelCountBuilder.buildCount(
|
||||
params.source,
|
||||
params.filters,
|
||||
);
|
||||
}
|
||||
|
||||
then<TResult1 = number, TResult2 = never>(
|
||||
onfulfilled?: ((value: number) => TResult1 | PromiseLike<TResult1>) | null | undefined,
|
||||
onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null | undefined,
|
||||
): Promise<TResult1 | TResult2> {
|
||||
return Promise.resolve(this.session.count(this.sql))
|
||||
.then(
|
||||
onfulfilled,
|
||||
onrejected,
|
||||
);
|
||||
}
|
||||
|
||||
catch(
|
||||
onRejected?: ((reason: any) => any) | null | undefined,
|
||||
): Promise<number> {
|
||||
return this.then(undefined, onRejected);
|
||||
}
|
||||
|
||||
finally(onFinally?: (() => void) | null | undefined): Promise<number> {
|
||||
return this.then(
|
||||
(value) => {
|
||||
onFinally?.();
|
||||
return value;
|
||||
},
|
||||
(reason) => {
|
||||
onFinally?.();
|
||||
throw reason;
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
import { entityKind } from '~/entity.ts';
|
||||
import type { GelDialect } from '~/gel-core/dialect.ts';
|
||||
import type {
|
||||
GelPreparedQuery,
|
||||
GelQueryResultHKT,
|
||||
GelQueryResultKind,
|
||||
GelSession,
|
||||
PreparedQueryConfig,
|
||||
} from '~/gel-core/session.ts';
|
||||
import type { GelTable } from '~/gel-core/table.ts';
|
||||
import type { SelectResultFields } from '~/query-builders/select.types.ts';
|
||||
import { QueryPromise } from '~/query-promise.ts';
|
||||
import type { RunnableQuery } from '~/runnable-query.ts';
|
||||
import type { Query, SQL, SQLWrapper } from '~/sql/sql.ts';
|
||||
import type { Subquery } from '~/subquery.ts';
|
||||
import { Table } from '~/table.ts';
|
||||
import { tracer } from '~/tracing.ts';
|
||||
import { orderSelectedFields } from '~/utils.ts';
|
||||
import type { GelColumn } from '../columns/common.ts';
|
||||
import { extractUsedTable } from '../utils.ts';
|
||||
import type { SelectedFieldsFlat, SelectedFieldsOrdered } from './select.types.ts';
|
||||
|
||||
export type GelDeleteWithout<
|
||||
T extends AnyGelDeleteBase,
|
||||
TDynamic extends boolean,
|
||||
K extends keyof T & string,
|
||||
> = TDynamic extends true ? T
|
||||
: Omit<
|
||||
GelDeleteBase<
|
||||
T['_']['table'],
|
||||
T['_']['queryResult'],
|
||||
T['_']['returning'],
|
||||
TDynamic,
|
||||
T['_']['excludedMethods'] | K
|
||||
>,
|
||||
T['_']['excludedMethods'] | K
|
||||
>;
|
||||
|
||||
export type GelDelete<
|
||||
TTable extends GelTable = GelTable,
|
||||
TQueryResult extends GelQueryResultHKT = GelQueryResultHKT,
|
||||
TReturning extends Record<string, unknown> | undefined = Record<string, unknown> | undefined,
|
||||
> = GelDeleteBase<TTable, TQueryResult, TReturning, true, never>;
|
||||
|
||||
export interface GelDeleteConfig {
|
||||
where?: SQL | undefined;
|
||||
table: GelTable;
|
||||
returning?: SelectedFieldsOrdered;
|
||||
withList?: Subquery[];
|
||||
}
|
||||
|
||||
export type GelDeleteReturningAll<
|
||||
T extends AnyGelDeleteBase,
|
||||
TDynamic extends boolean,
|
||||
> = GelDeleteWithout<
|
||||
GelDeleteBase<
|
||||
T['_']['table'],
|
||||
T['_']['queryResult'],
|
||||
T['_']['table']['$inferSelect'],
|
||||
TDynamic,
|
||||
T['_']['excludedMethods']
|
||||
>,
|
||||
TDynamic,
|
||||
'returning'
|
||||
>;
|
||||
|
||||
export type GelDeleteReturning<
|
||||
T extends AnyGelDeleteBase,
|
||||
TDynamic extends boolean,
|
||||
TSelectedFields extends SelectedFieldsFlat,
|
||||
> = GelDeleteWithout<
|
||||
GelDeleteBase<
|
||||
T['_']['table'],
|
||||
T['_']['queryResult'],
|
||||
SelectResultFields<TSelectedFields>,
|
||||
TDynamic,
|
||||
T['_']['excludedMethods']
|
||||
>,
|
||||
TDynamic,
|
||||
'returning'
|
||||
>;
|
||||
|
||||
export type GelDeletePrepare<T extends AnyGelDeleteBase> = GelPreparedQuery<
|
||||
PreparedQueryConfig & {
|
||||
execute: T['_']['returning'] extends undefined ? GelQueryResultKind<T['_']['queryResult'], never>
|
||||
: T['_']['returning'][];
|
||||
}
|
||||
>;
|
||||
|
||||
export type GelDeleteDynamic<T extends AnyGelDeleteBase> = GelDelete<
|
||||
T['_']['table'],
|
||||
T['_']['queryResult'],
|
||||
T['_']['returning']
|
||||
>;
|
||||
|
||||
export type AnyGelDeleteBase = GelDeleteBase<any, any, any, any, any>;
|
||||
|
||||
export interface GelDeleteBase<
|
||||
TTable extends GelTable,
|
||||
TQueryResult extends GelQueryResultHKT,
|
||||
TReturning extends Record<string, unknown> | undefined = undefined,
|
||||
TDynamic extends boolean = false,
|
||||
TExcludedMethods extends string = never,
|
||||
> extends
|
||||
QueryPromise<TReturning extends undefined ? GelQueryResultKind<TQueryResult, never> : TReturning[]>,
|
||||
RunnableQuery<TReturning extends undefined ? GelQueryResultKind<TQueryResult, never> : TReturning[], 'gel'>,
|
||||
SQLWrapper
|
||||
{
|
||||
readonly _: {
|
||||
dialect: 'gel';
|
||||
readonly table: TTable;
|
||||
readonly queryResult: TQueryResult;
|
||||
readonly returning: TReturning;
|
||||
readonly dynamic: TDynamic;
|
||||
readonly excludedMethods: TExcludedMethods;
|
||||
readonly result: TReturning extends undefined ? GelQueryResultKind<TQueryResult, never> : TReturning[];
|
||||
};
|
||||
}
|
||||
|
||||
export class GelDeleteBase<
|
||||
TTable extends GelTable,
|
||||
TQueryResult extends GelQueryResultHKT,
|
||||
TReturning extends Record<string, unknown> | undefined = undefined,
|
||||
TDynamic extends boolean = false,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
TExcludedMethods extends string = never,
|
||||
> extends QueryPromise<TReturning extends undefined ? GelQueryResultKind<TQueryResult, never> : TReturning[]>
|
||||
implements
|
||||
RunnableQuery<TReturning extends undefined ? GelQueryResultKind<TQueryResult, never> : TReturning[], 'gel'>,
|
||||
SQLWrapper
|
||||
{
|
||||
static override readonly [entityKind]: string = 'GelDelete';
|
||||
|
||||
private config: GelDeleteConfig;
|
||||
|
||||
constructor(
|
||||
table: TTable,
|
||||
private session: GelSession,
|
||||
private dialect: GelDialect,
|
||||
withList?: Subquery[],
|
||||
) {
|
||||
super();
|
||||
this.config = { table, withList };
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a `where` clause to the query.
|
||||
*
|
||||
* Calling this method will delete only those rows that fulfill a specified condition.
|
||||
*
|
||||
* See docs: {@link https://orm.drizzle.team/docs/delete}
|
||||
*
|
||||
* @param where the `where` clause.
|
||||
*
|
||||
* @example
|
||||
* You can use conditional operators and `sql function` to filter the rows to be deleted.
|
||||
*
|
||||
* ```ts
|
||||
* // Delete all cars with green color
|
||||
* await db.delete(cars).where(eq(cars.color, 'green'));
|
||||
* // or
|
||||
* await db.delete(cars).where(sql`${cars.color} = 'green'`)
|
||||
* ```
|
||||
*
|
||||
* You can logically combine conditional operators with `and()` and `or()` operators:
|
||||
*
|
||||
* ```ts
|
||||
* // Delete all BMW cars with a green color
|
||||
* await db.delete(cars).where(and(eq(cars.color, 'green'), eq(cars.brand, 'BMW')));
|
||||
*
|
||||
* // Delete all cars with the green or blue color
|
||||
* await db.delete(cars).where(or(eq(cars.color, 'green'), eq(cars.color, 'blue')));
|
||||
* ```
|
||||
*/
|
||||
where(where: SQL | undefined): GelDeleteWithout<this, TDynamic, 'where'> {
|
||||
this.config.where = where;
|
||||
return this as any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a `returning` clause to the query.
|
||||
*
|
||||
* Calling this method will return the specified fields of the deleted rows. If no fields are specified, all fields will be returned.
|
||||
*
|
||||
* See docs: {@link https://orm.drizzle.team/docs/delete#delete-with-return}
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // Delete all cars with the green color and return all fields
|
||||
* const deletedCars: Car[] = await db.delete(cars)
|
||||
* .where(eq(cars.color, 'green'))
|
||||
* .returning();
|
||||
*
|
||||
* // Delete all cars with the green color and return only their id and brand fields
|
||||
* const deletedCarsIdsAndBrands: { id: number, brand: string }[] = await db.delete(cars)
|
||||
* .where(eq(cars.color, 'green'))
|
||||
* .returning({ id: cars.id, brand: cars.brand });
|
||||
* ```
|
||||
*/
|
||||
returning(): GelDeleteReturningAll<this, TDynamic>;
|
||||
returning<TSelectedFields extends SelectedFieldsFlat>(
|
||||
fields: TSelectedFields,
|
||||
): GelDeleteReturning<this, TDynamic, TSelectedFields>;
|
||||
returning(
|
||||
fields: SelectedFieldsFlat = this.config.table[Table.Symbol.Columns],
|
||||
): GelDeleteReturning<this, TDynamic, any> {
|
||||
this.config.returning = orderSelectedFields<GelColumn>(fields);
|
||||
return this as any;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
getSQL(): SQL {
|
||||
return this.dialect.buildDeleteQuery(this.config);
|
||||
}
|
||||
|
||||
toSQL(): Query {
|
||||
const { typings: _typings, ...rest } = this.dialect.sqlToQuery(this.getSQL());
|
||||
return rest;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_prepare(name?: string): GelDeletePrepare<this> {
|
||||
return tracer.startActiveSpan('drizzle.prepareQuery', () => {
|
||||
return this.session.prepareQuery<
|
||||
PreparedQueryConfig & {
|
||||
execute: TReturning extends undefined ? GelQueryResultKind<TQueryResult, never> : TReturning[];
|
||||
}
|
||||
>(this.dialect.sqlToQuery(this.getSQL()), this.config.returning, name, true, undefined, {
|
||||
type: 'delete',
|
||||
tables: extractUsedTable(this.config.table),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
prepare(name: string): GelDeletePrepare<this> {
|
||||
return this._prepare(name);
|
||||
}
|
||||
|
||||
override execute: ReturnType<this['prepare']>['execute'] = (placeholderValues) => {
|
||||
return tracer.startActiveSpan('drizzle.operation', () => {
|
||||
return this._prepare().execute(placeholderValues);
|
||||
});
|
||||
};
|
||||
|
||||
$dynamic(): GelDeleteDynamic<this> {
|
||||
return this as any;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export * from './delete.ts';
|
||||
export * from './insert.ts';
|
||||
export * from './query-builder.ts';
|
||||
export * from './refresh-materialized-view.ts';
|
||||
export * from './select.ts';
|
||||
export * from './select.types.ts';
|
||||
export * from './update.ts';
|
||||
@@ -0,0 +1,410 @@
|
||||
import { entityKind, is } from '~/entity.ts';
|
||||
import type { GelDialect } from '~/gel-core/dialect.ts';
|
||||
import type { IndexColumn } from '~/gel-core/indexes.ts';
|
||||
import type {
|
||||
GelPreparedQuery,
|
||||
GelQueryResultHKT,
|
||||
GelQueryResultKind,
|
||||
GelSession,
|
||||
PreparedQueryConfig,
|
||||
} from '~/gel-core/session.ts';
|
||||
import type { GelTable, TableConfig } from '~/gel-core/table.ts';
|
||||
import type { TypedQueryBuilder } from '~/query-builders/query-builder.ts';
|
||||
import type { SelectResultFields } from '~/query-builders/select.types.ts';
|
||||
import { QueryPromise } from '~/query-promise.ts';
|
||||
import type { RunnableQuery } from '~/runnable-query.ts';
|
||||
import type { Placeholder, Query, SQLWrapper } from '~/sql/sql.ts';
|
||||
import { Param, SQL } from '~/sql/sql.ts';
|
||||
import type { Subquery } from '~/subquery.ts';
|
||||
import type { InferInsertModel } from '~/table.ts';
|
||||
import { Columns, Table } from '~/table.ts';
|
||||
import { tracer } from '~/tracing.ts';
|
||||
import { haveSameKeys, type NeonAuthToken, orderSelectedFields } from '~/utils.ts';
|
||||
import type { AnyGelColumn, GelColumn } from '../columns/common.ts';
|
||||
import { extractUsedTable } from '../utils.ts';
|
||||
import { QueryBuilder } from './query-builder.ts';
|
||||
import type { SelectedFieldsFlat, SelectedFieldsOrdered } from './select.types.ts';
|
||||
import type { GelUpdateSetSource } from './update.ts';
|
||||
|
||||
export interface GelInsertConfig<TTable extends GelTable = GelTable> {
|
||||
table: TTable;
|
||||
values: Record<string, Param | SQL>[] | GelInsertSelectQueryBuilder<TTable> | SQL;
|
||||
withList?: Subquery[];
|
||||
onConflict?: SQL;
|
||||
returning?: SelectedFieldsOrdered;
|
||||
select?: boolean;
|
||||
overridingSystemValue_?: boolean;
|
||||
}
|
||||
|
||||
export type GelInsertValue<TTable extends GelTable<TableConfig>, OverrideT extends boolean = false> =
|
||||
& {
|
||||
[Key in keyof InferInsertModel<TTable, { dbColumnNames: false; override: OverrideT }>]:
|
||||
| InferInsertModel<TTable, { dbColumnNames: false; override: OverrideT }>[Key]
|
||||
| SQL
|
||||
| Placeholder;
|
||||
}
|
||||
& {};
|
||||
|
||||
export type GelInsertSelectQueryBuilder<TTable extends GelTable> = TypedQueryBuilder<
|
||||
{ [K in keyof TTable['$inferInsert']]: AnyGelColumn | SQL | SQL.Aliased | TTable['$inferInsert'][K] }
|
||||
>;
|
||||
|
||||
export class GelInsertBuilder<
|
||||
TTable extends GelTable,
|
||||
TQueryResult extends GelQueryResultHKT,
|
||||
OverrideT extends boolean = false,
|
||||
> {
|
||||
static readonly [entityKind]: string = 'GelInsertBuilder';
|
||||
|
||||
constructor(
|
||||
private table: TTable,
|
||||
private session: GelSession,
|
||||
private dialect: GelDialect,
|
||||
private withList?: Subquery[],
|
||||
private overridingSystemValue_?: boolean,
|
||||
) {}
|
||||
|
||||
private authToken?: NeonAuthToken;
|
||||
/** @internal */
|
||||
setToken(token?: NeonAuthToken) {
|
||||
this.authToken = token;
|
||||
return this;
|
||||
}
|
||||
|
||||
overridingSystemValue(): Omit<GelInsertBuilder<TTable, TQueryResult, true>, 'overridingSystemValue'> {
|
||||
this.overridingSystemValue_ = true;
|
||||
return this as any;
|
||||
}
|
||||
|
||||
values(value: GelInsertValue<TTable, OverrideT>): GelInsertBase<TTable, TQueryResult>;
|
||||
values(values: GelInsertValue<TTable, OverrideT>[]): GelInsertBase<TTable, TQueryResult>;
|
||||
values(
|
||||
values: GelInsertValue<TTable, OverrideT> | GelInsertValue<TTable, OverrideT>[],
|
||||
): GelInsertBase<TTable, TQueryResult> {
|
||||
values = Array.isArray(values) ? values : [values];
|
||||
if (values.length === 0) {
|
||||
throw new Error('values() must be called with at least one value');
|
||||
}
|
||||
const mappedValues = values.map((entry) => {
|
||||
const result: Record<string, Param | SQL> = {};
|
||||
const cols = this.table[Table.Symbol.Columns];
|
||||
for (const colKey of Object.keys(entry)) {
|
||||
const colValue = entry[colKey as keyof typeof entry];
|
||||
result[colKey] = is(colValue, SQL) ? colValue : new Param(colValue, cols[colKey]);
|
||||
}
|
||||
return result;
|
||||
});
|
||||
|
||||
return new GelInsertBase(
|
||||
this.table,
|
||||
mappedValues,
|
||||
this.session,
|
||||
this.dialect,
|
||||
this.withList,
|
||||
false,
|
||||
this.overridingSystemValue_,
|
||||
);
|
||||
}
|
||||
|
||||
select(selectQuery: (qb: QueryBuilder) => GelInsertSelectQueryBuilder<TTable>): GelInsertBase<TTable, TQueryResult>;
|
||||
select(selectQuery: (qb: QueryBuilder) => SQL): GelInsertBase<TTable, TQueryResult>;
|
||||
select(selectQuery: SQL): GelInsertBase<TTable, TQueryResult>;
|
||||
select(selectQuery: GelInsertSelectQueryBuilder<TTable>): GelInsertBase<TTable, TQueryResult>;
|
||||
select(
|
||||
selectQuery:
|
||||
| SQL
|
||||
| GelInsertSelectQueryBuilder<TTable>
|
||||
| ((qb: QueryBuilder) => GelInsertSelectQueryBuilder<TTable> | SQL),
|
||||
): GelInsertBase<TTable, TQueryResult> {
|
||||
const select = typeof selectQuery === 'function' ? selectQuery(new QueryBuilder()) : selectQuery;
|
||||
|
||||
if (
|
||||
!is(select, SQL)
|
||||
&& !haveSameKeys(this.table[Columns], select._.selectedFields)
|
||||
) {
|
||||
throw new Error(
|
||||
'Insert select error: selected fields are not the same or are in a different order compared to the table definition',
|
||||
);
|
||||
}
|
||||
|
||||
return new GelInsertBase(this.table, select, this.session, this.dialect, this.withList, true);
|
||||
}
|
||||
}
|
||||
|
||||
export type GelInsertWithout<T extends AnyGelInsert, TDynamic extends boolean, K extends keyof T & string> =
|
||||
TDynamic extends true ? T
|
||||
: Omit<
|
||||
GelInsertBase<
|
||||
T['_']['table'],
|
||||
T['_']['queryResult'],
|
||||
T['_']['returning'],
|
||||
TDynamic,
|
||||
T['_']['excludedMethods'] | K
|
||||
>,
|
||||
T['_']['excludedMethods'] | K
|
||||
>;
|
||||
|
||||
export type GelInsertReturning<
|
||||
T extends AnyGelInsert,
|
||||
TDynamic extends boolean,
|
||||
TSelectedFields extends SelectedFieldsFlat,
|
||||
> = GelInsertBase<
|
||||
T['_']['table'],
|
||||
T['_']['queryResult'],
|
||||
SelectResultFields<TSelectedFields>,
|
||||
TDynamic,
|
||||
T['_']['excludedMethods']
|
||||
>;
|
||||
|
||||
export type GelInsertReturningAll<T extends AnyGelInsert, TDynamic extends boolean> = GelInsertBase<
|
||||
T['_']['table'],
|
||||
T['_']['queryResult'],
|
||||
T['_']['table']['$inferSelect'],
|
||||
TDynamic,
|
||||
T['_']['excludedMethods']
|
||||
>;
|
||||
|
||||
export interface GelInsertOnConflictDoUpdateConfig<T extends AnyGelInsert> {
|
||||
target: IndexColumn | IndexColumn[];
|
||||
/** @deprecated use either `targetWhere` or `setWhere` */
|
||||
where?: SQL;
|
||||
// TODO: add tests for targetWhere and setWhere
|
||||
targetWhere?: SQL;
|
||||
setWhere?: SQL;
|
||||
set: GelUpdateSetSource<T['_']['table']>;
|
||||
}
|
||||
|
||||
export type GelInsertPrepare<T extends AnyGelInsert> = GelPreparedQuery<
|
||||
PreparedQueryConfig & {
|
||||
execute: T['_']['returning'] extends undefined ? GelQueryResultKind<T['_']['queryResult'], never>
|
||||
: T['_']['returning'][];
|
||||
}
|
||||
>;
|
||||
|
||||
export type GelInsertDynamic<T extends AnyGelInsert> = GelInsert<
|
||||
T['_']['table'],
|
||||
T['_']['queryResult'],
|
||||
T['_']['returning']
|
||||
>;
|
||||
|
||||
export type AnyGelInsert = GelInsertBase<any, any, any, any, any>;
|
||||
|
||||
export type GelInsert<
|
||||
TTable extends GelTable = GelTable,
|
||||
TQueryResult extends GelQueryResultHKT = GelQueryResultHKT,
|
||||
TReturning extends Record<string, unknown> | undefined = Record<string, unknown> | undefined,
|
||||
> = GelInsertBase<TTable, TQueryResult, TReturning, true, never>;
|
||||
|
||||
export interface GelInsertBase<
|
||||
TTable extends GelTable,
|
||||
TQueryResult extends GelQueryResultHKT,
|
||||
TReturning extends Record<string, unknown> | undefined = undefined,
|
||||
TDynamic extends boolean = false,
|
||||
TExcludedMethods extends string = never,
|
||||
> extends
|
||||
QueryPromise<TReturning extends undefined ? GelQueryResultKind<TQueryResult, never> : TReturning[]>,
|
||||
RunnableQuery<TReturning extends undefined ? GelQueryResultKind<TQueryResult, never> : TReturning[], 'gel'>,
|
||||
SQLWrapper
|
||||
{
|
||||
readonly _: {
|
||||
readonly dialect: 'gel';
|
||||
readonly table: TTable;
|
||||
readonly queryResult: TQueryResult;
|
||||
readonly returning: TReturning;
|
||||
readonly dynamic: TDynamic;
|
||||
readonly excludedMethods: TExcludedMethods;
|
||||
readonly result: TReturning extends undefined ? GelQueryResultKind<TQueryResult, never> : TReturning[];
|
||||
};
|
||||
}
|
||||
|
||||
export class GelInsertBase<
|
||||
TTable extends GelTable,
|
||||
TQueryResult extends GelQueryResultHKT,
|
||||
TReturning extends Record<string, unknown> | undefined = undefined,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
TDynamic extends boolean = false,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
TExcludedMethods extends string = never,
|
||||
> extends QueryPromise<TReturning extends undefined ? GelQueryResultKind<TQueryResult, never> : TReturning[]>
|
||||
implements
|
||||
RunnableQuery<TReturning extends undefined ? GelQueryResultKind<TQueryResult, never> : TReturning[], 'gel'>,
|
||||
SQLWrapper
|
||||
{
|
||||
static override readonly [entityKind]: string = 'GelInsert';
|
||||
|
||||
private config: GelInsertConfig<TTable>;
|
||||
|
||||
constructor(
|
||||
table: TTable,
|
||||
values: GelInsertConfig['values'],
|
||||
private session: GelSession,
|
||||
private dialect: GelDialect,
|
||||
withList?: Subquery[],
|
||||
select?: boolean,
|
||||
overridingSystemValue_?: boolean,
|
||||
) {
|
||||
super();
|
||||
this.config = { table, values: values as any, withList, select, overridingSystemValue_ };
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a `returning` clause to the query.
|
||||
*
|
||||
* Calling this method will return the specified fields of the inserted rows. If no fields are specified, all fields will be returned.
|
||||
*
|
||||
* See docs: {@link https://orm.drizzle.team/docs/insert#insert-returning}
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // Insert one row and return all fields
|
||||
* const insertedCar: Car[] = await db.insert(cars)
|
||||
* .values({ brand: 'BMW' })
|
||||
* .returning();
|
||||
*
|
||||
* // Insert one row and return only the id
|
||||
* const insertedCarId: { id: number }[] = await db.insert(cars)
|
||||
* .values({ brand: 'BMW' })
|
||||
* .returning({ id: cars.id });
|
||||
* ```
|
||||
*/
|
||||
returning(): GelInsertWithout<GelInsertReturningAll<this, TDynamic>, TDynamic, 'returning'>;
|
||||
returning<TSelectedFields extends SelectedFieldsFlat>(
|
||||
fields: TSelectedFields,
|
||||
): GelInsertWithout<GelInsertReturning<this, TDynamic, TSelectedFields>, TDynamic, 'returning'>;
|
||||
returning(
|
||||
fields: SelectedFieldsFlat = this.config.table[Table.Symbol.Columns],
|
||||
): GelInsertWithout<AnyGelInsert, TDynamic, 'returning'> {
|
||||
this.config.returning = orderSelectedFields<GelColumn>(fields);
|
||||
return this as any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an `on conflict do nothing` clause to the query.
|
||||
*
|
||||
* Calling this method simply avoids inserting a row as its alternative action.
|
||||
*
|
||||
* See docs: {@link https://orm.drizzle.team/docs/insert#on-conflict-do-nothing}
|
||||
*
|
||||
* @param config The `target` and `where` clauses.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // Insert one row and cancel the insert if there's a conflict
|
||||
* await db.insert(cars)
|
||||
* .values({ id: 1, brand: 'BMW' })
|
||||
* .onConflictDoNothing();
|
||||
*
|
||||
* // Explicitly specify conflict target
|
||||
* await db.insert(cars)
|
||||
* .values({ id: 1, brand: 'BMW' })
|
||||
* .onConflictDoNothing({ target: cars.id });
|
||||
* ```
|
||||
*/
|
||||
// TODO not supported
|
||||
// onConflictDoNothing(
|
||||
// config: { target?: IndexColumn | IndexColumn[]; where?: SQL } = {},
|
||||
// ): GelInsertWithout<this, TDynamic, 'onConflictDoNothing' | 'onConflictDoUpdate'> {
|
||||
// if (config.target === undefined) {
|
||||
// this.config.onConflict = sql`do nothing`;
|
||||
// } else {
|
||||
// let targetColumn = '';
|
||||
// targetColumn = Array.isArray(config.target)
|
||||
// ? config.target.map((it) => this.dialect.escapeName(this.dialect.casing.getColumnCasing(it))).join(',')
|
||||
// : this.dialect.escapeName(this.dialect.casing.getColumnCasing(config.target));
|
||||
|
||||
// const whereSql = config.where ? sql` where ${config.where}` : undefined;
|
||||
// this.config.onConflict = sql`(${sql.raw(targetColumn)})${whereSql} do nothing`;
|
||||
// }
|
||||
// return this as any;
|
||||
// }
|
||||
|
||||
/**
|
||||
* Adds an `on conflict do update` clause to the query.
|
||||
*
|
||||
* Calling this method will update the existing row that conflicts with the row proposed for insertion as its alternative action.
|
||||
*
|
||||
* See docs: {@link https://orm.drizzle.team/docs/insert#upserts-and-conflicts}
|
||||
*
|
||||
* @param config The `target`, `set` and `where` clauses.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // Update the row if there's a conflict
|
||||
* await db.insert(cars)
|
||||
* .values({ id: 1, brand: 'BMW' })
|
||||
* .onConflictDoUpdate({
|
||||
* target: cars.id,
|
||||
* set: { brand: 'Porsche' }
|
||||
* });
|
||||
*
|
||||
* // Upsert with 'where' clause
|
||||
* await db.insert(cars)
|
||||
* .values({ id: 1, brand: 'BMW' })
|
||||
* .onConflictDoUpdate({
|
||||
* target: cars.id,
|
||||
* set: { brand: 'newBMW' },
|
||||
* targetWhere: sql`${cars.createdAt} > '2023-01-01'::date`,
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
// TODO not supported
|
||||
// onConflictDoUpdate(
|
||||
// config: GelInsertOnConflictDoUpdateConfig<this>,
|
||||
// ): GelInsertWithout<this, TDynamic, 'onConflictDoNothing' | 'onConflictDoUpdate'> {
|
||||
// if (config.where && (config.targetWhere || config.setWhere)) {
|
||||
// throw new Error(
|
||||
// 'You cannot use both "where" and "targetWhere"/"setWhere" at the same time - "where" is deprecated, use "targetWhere" or "setWhere" instead.',
|
||||
// );
|
||||
// }
|
||||
// const whereSql = config.where ? sql` where ${config.where}` : undefined;
|
||||
// const targetWhereSql = config.targetWhere ? sql` where ${config.targetWhere}` : undefined;
|
||||
// const setWhereSql = config.setWhere ? sql` where ${config.setWhere}` : undefined;
|
||||
// const setSql = this.dialect.buildUpdateSet(this.config.table, mapUpdateSet(this.config.table, config.set));
|
||||
// let targetColumn = '';
|
||||
// targetColumn = Array.isArray(config.target)
|
||||
// ? config.target.map((it) => this.dialect.escapeName(this.dialect.casing.getColumnCasing(it))).join(',')
|
||||
// : this.dialect.escapeName(this.dialect.casing.getColumnCasing(config.target));
|
||||
// this.config.onConflict = sql`(${
|
||||
// sql.raw(targetColumn)
|
||||
// })${targetWhereSql} do update set ${setSql}${whereSql}${setWhereSql}`;
|
||||
// return this as any;
|
||||
// }
|
||||
|
||||
/** @internal */
|
||||
getSQL(): SQL {
|
||||
return this.dialect.buildInsertQuery(this.config);
|
||||
}
|
||||
|
||||
toSQL(): Query {
|
||||
const { typings: _typings, ...rest } = this.dialect.sqlToQuery(this.getSQL());
|
||||
return rest;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_prepare(name?: string): GelInsertPrepare<this> {
|
||||
return tracer.startActiveSpan('drizzle.prepareQuery', () => {
|
||||
return this.session.prepareQuery<
|
||||
PreparedQueryConfig & {
|
||||
execute: TReturning extends undefined ? GelQueryResultKind<TQueryResult, never> : TReturning[];
|
||||
}
|
||||
>(this.dialect.sqlToQuery(this.getSQL()), this.config.returning, name, true, undefined, {
|
||||
type: 'insert',
|
||||
tables: extractUsedTable(this.config.table),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
prepare(name: string): GelInsertPrepare<this> {
|
||||
return this._prepare(name);
|
||||
}
|
||||
|
||||
override execute: ReturnType<this['prepare']>['execute'] = (placeholderValues) => {
|
||||
return tracer.startActiveSpan('drizzle.operation', () => {
|
||||
return this._prepare().execute(placeholderValues);
|
||||
});
|
||||
};
|
||||
|
||||
$dynamic(): GelInsertDynamic<this> {
|
||||
return this as any;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
import { entityKind, is } from '~/entity.ts';
|
||||
import type { GelDialectConfig } from '~/gel-core/dialect.ts';
|
||||
import { GelDialect } from '~/gel-core/dialect.ts';
|
||||
import type { TypedQueryBuilder } from '~/query-builders/query-builder.ts';
|
||||
import { SelectionProxyHandler } from '~/selection-proxy.ts';
|
||||
import type { ColumnsSelection, SQLWrapper } from '~/sql/sql.ts';
|
||||
import { WithSubquery } from '~/subquery.ts';
|
||||
import type { GelColumn } from '../columns/index.ts';
|
||||
import type { WithSubqueryWithSelection } from '../subquery.ts';
|
||||
import { GelSelectBuilder } from './select.ts';
|
||||
import type { SelectedFields } from './select.types.ts';
|
||||
|
||||
export class QueryBuilder {
|
||||
static readonly [entityKind]: string = 'GelQueryBuilder';
|
||||
|
||||
private dialect: GelDialect | undefined;
|
||||
private dialectConfig: GelDialectConfig | undefined;
|
||||
|
||||
constructor(dialect?: GelDialect | GelDialectConfig) {
|
||||
this.dialect = is(dialect, GelDialect) ? dialect : undefined;
|
||||
this.dialectConfig = is(dialect, GelDialect) ? undefined : dialect;
|
||||
}
|
||||
|
||||
$with<TAlias extends string>(alias: TAlias) {
|
||||
const queryBuilder = this;
|
||||
|
||||
return {
|
||||
as<TSelection extends ColumnsSelection>(
|
||||
qb: TypedQueryBuilder<TSelection> | ((qb: QueryBuilder) => TypedQueryBuilder<TSelection>),
|
||||
): WithSubqueryWithSelection<TSelection, TAlias> {
|
||||
if (typeof qb === 'function') {
|
||||
qb = qb(queryBuilder);
|
||||
}
|
||||
|
||||
return new Proxy(
|
||||
new WithSubquery(qb.getSQL(), qb.getSelectedFields() as SelectedFields, alias, true),
|
||||
new SelectionProxyHandler({ alias, sqlAliasedBehavior: 'alias', sqlBehavior: 'error' }),
|
||||
) as WithSubqueryWithSelection<TSelection, TAlias>;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
with(...queries: WithSubquery[]) {
|
||||
const self = this;
|
||||
|
||||
function select(): GelSelectBuilder<undefined, 'qb'>;
|
||||
function select<TSelection extends SelectedFields>(fields: TSelection): GelSelectBuilder<TSelection, 'qb'>;
|
||||
function select<TSelection extends SelectedFields>(
|
||||
fields?: TSelection,
|
||||
): GelSelectBuilder<TSelection | undefined, 'qb'> {
|
||||
return new GelSelectBuilder({
|
||||
fields: fields ?? undefined,
|
||||
session: undefined,
|
||||
dialect: self.getDialect(),
|
||||
withList: queries,
|
||||
});
|
||||
}
|
||||
|
||||
function selectDistinct(): GelSelectBuilder<undefined, 'qb'>;
|
||||
function selectDistinct<TSelection extends SelectedFields>(fields: TSelection): GelSelectBuilder<TSelection, 'qb'>;
|
||||
function selectDistinct(fields?: SelectedFields): GelSelectBuilder<SelectedFields | undefined, 'qb'> {
|
||||
return new GelSelectBuilder({
|
||||
fields: fields ?? undefined,
|
||||
session: undefined,
|
||||
dialect: self.getDialect(),
|
||||
distinct: true,
|
||||
});
|
||||
}
|
||||
|
||||
function selectDistinctOn(on: (GelColumn | SQLWrapper)[]): GelSelectBuilder<undefined, 'qb'>;
|
||||
function selectDistinctOn<TSelection extends SelectedFields>(
|
||||
on: (GelColumn | SQLWrapper)[],
|
||||
fields: TSelection,
|
||||
): GelSelectBuilder<TSelection, 'qb'>;
|
||||
function selectDistinctOn(
|
||||
on: (GelColumn | SQLWrapper)[],
|
||||
fields?: SelectedFields,
|
||||
): GelSelectBuilder<SelectedFields | undefined, 'qb'> {
|
||||
return new GelSelectBuilder({
|
||||
fields: fields ?? undefined,
|
||||
session: undefined,
|
||||
dialect: self.getDialect(),
|
||||
distinct: { on },
|
||||
});
|
||||
}
|
||||
|
||||
return { select, selectDistinct, selectDistinctOn };
|
||||
}
|
||||
|
||||
select(): GelSelectBuilder<undefined, 'qb'>;
|
||||
select<TSelection extends SelectedFields>(fields: TSelection): GelSelectBuilder<TSelection, 'qb'>;
|
||||
select<TSelection extends SelectedFields>(fields?: TSelection): GelSelectBuilder<TSelection | undefined, 'qb'> {
|
||||
return new GelSelectBuilder({
|
||||
fields: fields ?? undefined,
|
||||
session: undefined,
|
||||
dialect: this.getDialect(),
|
||||
});
|
||||
}
|
||||
|
||||
selectDistinct(): GelSelectBuilder<undefined>;
|
||||
selectDistinct<TSelection extends SelectedFields>(fields: TSelection): GelSelectBuilder<TSelection>;
|
||||
selectDistinct(fields?: SelectedFields): GelSelectBuilder<SelectedFields | undefined> {
|
||||
return new GelSelectBuilder({
|
||||
fields: fields ?? undefined,
|
||||
session: undefined,
|
||||
dialect: this.getDialect(),
|
||||
distinct: true,
|
||||
});
|
||||
}
|
||||
|
||||
selectDistinctOn(on: (GelColumn | SQLWrapper)[]): GelSelectBuilder<undefined>;
|
||||
selectDistinctOn<TSelection extends SelectedFields>(
|
||||
on: (GelColumn | SQLWrapper)[],
|
||||
fields: TSelection,
|
||||
): GelSelectBuilder<TSelection>;
|
||||
selectDistinctOn(
|
||||
on: (GelColumn | SQLWrapper)[],
|
||||
fields?: SelectedFields,
|
||||
): GelSelectBuilder<SelectedFields | undefined> {
|
||||
return new GelSelectBuilder({
|
||||
fields: fields ?? undefined,
|
||||
session: undefined,
|
||||
dialect: this.getDialect(),
|
||||
distinct: { on },
|
||||
});
|
||||
}
|
||||
|
||||
// Lazy load dialect to avoid circular dependency
|
||||
private getDialect() {
|
||||
if (!this.dialect) {
|
||||
this.dialect = new GelDialect(this.dialectConfig);
|
||||
}
|
||||
|
||||
return this.dialect;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
import { entityKind } from '~/entity.ts';
|
||||
import { QueryPromise } from '~/query-promise.ts';
|
||||
import {
|
||||
type BuildQueryResult,
|
||||
type BuildRelationalQueryResult,
|
||||
type DBQueryConfig,
|
||||
mapRelationalRow,
|
||||
type TableRelationalConfig,
|
||||
type TablesRelationalConfig,
|
||||
} from '~/relations.ts';
|
||||
import type { RunnableQuery } from '~/runnable-query.ts';
|
||||
import type { Query, QueryWithTypings, SQL, SQLWrapper } from '~/sql/sql.ts';
|
||||
import { tracer } from '~/tracing.ts';
|
||||
import type { KnownKeysOnly } from '~/utils.ts';
|
||||
import type { GelDialect } from '../dialect.ts';
|
||||
import type { GelPreparedQuery, GelSession, PreparedQueryConfig } from '../session.ts';
|
||||
import type { GelTable } from '../table.ts';
|
||||
|
||||
export class RelationalQueryBuilder<TSchema extends TablesRelationalConfig, TFields extends TableRelationalConfig> {
|
||||
static readonly [entityKind]: string = 'GelRelationalQueryBuilder';
|
||||
|
||||
constructor(
|
||||
private fullSchema: Record<string, unknown>,
|
||||
private schema: TSchema,
|
||||
private tableNamesMap: Record<string, string>,
|
||||
private table: GelTable,
|
||||
private tableConfig: TableRelationalConfig,
|
||||
private dialect: GelDialect,
|
||||
private session: GelSession,
|
||||
) {}
|
||||
|
||||
findMany<TConfig extends DBQueryConfig<'many', true, TSchema, TFields>>(
|
||||
config?: KnownKeysOnly<TConfig, DBQueryConfig<'many', true, TSchema, TFields>>,
|
||||
): GelRelationalQuery<BuildQueryResult<TSchema, TFields, TConfig>[]> {
|
||||
return new GelRelationalQuery(
|
||||
this.fullSchema,
|
||||
this.schema,
|
||||
this.tableNamesMap,
|
||||
this.table,
|
||||
this.tableConfig,
|
||||
this.dialect,
|
||||
this.session,
|
||||
config ? (config as DBQueryConfig<'many', true>) : {},
|
||||
'many',
|
||||
);
|
||||
}
|
||||
|
||||
findFirst<TSelection extends Omit<DBQueryConfig<'many', true, TSchema, TFields>, 'limit'>>(
|
||||
config?: KnownKeysOnly<TSelection, Omit<DBQueryConfig<'many', true, TSchema, TFields>, 'limit'>>,
|
||||
): GelRelationalQuery<BuildQueryResult<TSchema, TFields, TSelection> | undefined> {
|
||||
return new GelRelationalQuery(
|
||||
this.fullSchema,
|
||||
this.schema,
|
||||
this.tableNamesMap,
|
||||
this.table,
|
||||
this.tableConfig,
|
||||
this.dialect,
|
||||
this.session,
|
||||
config ? { ...(config as DBQueryConfig<'many', true> | undefined), limit: 1 } : { limit: 1 },
|
||||
'first',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class GelRelationalQuery<TResult> extends QueryPromise<TResult>
|
||||
implements RunnableQuery<TResult, 'gel'>, SQLWrapper
|
||||
{
|
||||
static override readonly [entityKind]: string = 'GelRelationalQuery';
|
||||
|
||||
declare readonly _: {
|
||||
readonly dialect: 'gel';
|
||||
readonly result: TResult;
|
||||
};
|
||||
|
||||
constructor(
|
||||
private fullSchema: Record<string, unknown>,
|
||||
private schema: TablesRelationalConfig,
|
||||
private tableNamesMap: Record<string, string>,
|
||||
private table: GelTable,
|
||||
private tableConfig: TableRelationalConfig,
|
||||
private dialect: GelDialect,
|
||||
private session: GelSession,
|
||||
private config: DBQueryConfig<'many', true> | true,
|
||||
private mode: 'many' | 'first',
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_prepare(name?: string): GelPreparedQuery<PreparedQueryConfig & { execute: TResult }> {
|
||||
return tracer.startActiveSpan('drizzle.prepareQuery', () => {
|
||||
const { query, builtQuery } = this._toSQL();
|
||||
|
||||
return this.session.prepareQuery<PreparedQueryConfig & { execute: TResult }>(
|
||||
builtQuery,
|
||||
undefined,
|
||||
name,
|
||||
true,
|
||||
(rawRows, mapColumnValue) => {
|
||||
const rows = rawRows.map((row) =>
|
||||
mapRelationalRow(this.schema, this.tableConfig, row, query.selection, mapColumnValue)
|
||||
);
|
||||
if (this.mode === 'first') {
|
||||
return rows[0] as TResult;
|
||||
}
|
||||
return rows as TResult;
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
prepare(name: string): GelPreparedQuery<PreparedQueryConfig & { execute: TResult }> {
|
||||
return this._prepare(name);
|
||||
}
|
||||
|
||||
private _getQuery() {
|
||||
return this.dialect.buildRelationalQueryWithoutPK({
|
||||
fullSchema: this.fullSchema,
|
||||
schema: this.schema,
|
||||
tableNamesMap: this.tableNamesMap,
|
||||
table: this.table,
|
||||
tableConfig: this.tableConfig,
|
||||
queryConfig: this.config,
|
||||
tableAlias: this.tableConfig.tsName,
|
||||
});
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
getSQL(): SQL {
|
||||
return this._getQuery().sql as SQL;
|
||||
}
|
||||
|
||||
private _toSQL(): { query: BuildRelationalQueryResult; builtQuery: QueryWithTypings } {
|
||||
const query = this._getQuery();
|
||||
|
||||
const builtQuery = this.dialect.sqlToQuery(query.sql as SQL);
|
||||
|
||||
return { query, builtQuery };
|
||||
}
|
||||
|
||||
toSQL(): Query {
|
||||
return this._toSQL().builtQuery;
|
||||
}
|
||||
|
||||
override execute(): Promise<TResult> {
|
||||
return tracer.startActiveSpan('drizzle.operation', () => {
|
||||
return this._prepare().execute(undefined);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { entityKind } from '~/entity.ts';
|
||||
import { QueryPromise } from '~/query-promise.ts';
|
||||
import type { RunnableQuery } from '~/runnable-query.ts';
|
||||
import type { PreparedQuery } from '~/session.ts';
|
||||
import type { Query, SQL, SQLWrapper } from '~/sql/sql.ts';
|
||||
|
||||
export interface GelRaw<TResult> extends QueryPromise<TResult>, RunnableQuery<TResult, 'gel'>, SQLWrapper {}
|
||||
|
||||
export class GelRaw<TResult> extends QueryPromise<TResult>
|
||||
implements RunnableQuery<TResult, 'gel'>, SQLWrapper, PreparedQuery
|
||||
{
|
||||
static override readonly [entityKind]: string = 'GelRaw';
|
||||
|
||||
declare readonly _: {
|
||||
readonly dialect: 'gel';
|
||||
readonly result: TResult;
|
||||
};
|
||||
|
||||
constructor(
|
||||
public execute: () => Promise<TResult>,
|
||||
private sql: SQL,
|
||||
private query: Query,
|
||||
private mapBatchResult: (result: unknown) => unknown,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
getSQL() {
|
||||
return this.sql;
|
||||
}
|
||||
|
||||
getQuery() {
|
||||
return this.query;
|
||||
}
|
||||
|
||||
mapResult(result: unknown, isFromBatch?: boolean) {
|
||||
return isFromBatch ? this.mapBatchResult(result) : result;
|
||||
}
|
||||
|
||||
_prepare(): PreparedQuery {
|
||||
return this;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
isResponseInArrayMode() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import { entityKind } from '~/entity.ts';
|
||||
import type { GelDialect } from '~/gel-core/dialect.ts';
|
||||
import type {
|
||||
GelPreparedQuery,
|
||||
GelQueryResultHKT,
|
||||
GelQueryResultKind,
|
||||
GelSession,
|
||||
PreparedQueryConfig,
|
||||
} from '~/gel-core/session.ts';
|
||||
import type { GelMaterializedView } from '~/gel-core/view.ts';
|
||||
import { QueryPromise } from '~/query-promise.ts';
|
||||
import type { RunnableQuery } from '~/runnable-query.ts';
|
||||
import type { Query, SQL, SQLWrapper } from '~/sql/sql.ts';
|
||||
import { tracer } from '~/tracing.ts';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface GelRefreshMaterializedView<TQueryResult extends GelQueryResultHKT>
|
||||
extends
|
||||
QueryPromise<GelQueryResultKind<TQueryResult, never>>,
|
||||
RunnableQuery<GelQueryResultKind<TQueryResult, never>, 'gel'>,
|
||||
SQLWrapper
|
||||
{
|
||||
readonly _: {
|
||||
readonly dialect: 'gel';
|
||||
readonly result: GelQueryResultKind<TQueryResult, never>;
|
||||
};
|
||||
}
|
||||
|
||||
export class GelRefreshMaterializedView<TQueryResult extends GelQueryResultHKT>
|
||||
extends QueryPromise<GelQueryResultKind<TQueryResult, never>>
|
||||
implements RunnableQuery<GelQueryResultKind<TQueryResult, never>, 'gel'>, SQLWrapper
|
||||
{
|
||||
static override readonly [entityKind]: string = 'GelRefreshMaterializedView';
|
||||
|
||||
private config: {
|
||||
view: GelMaterializedView;
|
||||
concurrently?: boolean;
|
||||
withNoData?: boolean;
|
||||
};
|
||||
|
||||
constructor(
|
||||
view: GelMaterializedView,
|
||||
private session: GelSession,
|
||||
private dialect: GelDialect,
|
||||
) {
|
||||
super();
|
||||
this.config = { view };
|
||||
}
|
||||
|
||||
concurrently(): this {
|
||||
if (this.config.withNoData !== undefined) {
|
||||
throw new Error('Cannot use concurrently and withNoData together');
|
||||
}
|
||||
this.config.concurrently = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
withNoData(): this {
|
||||
if (this.config.concurrently !== undefined) {
|
||||
throw new Error('Cannot use concurrently and withNoData together');
|
||||
}
|
||||
this.config.withNoData = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
getSQL(): SQL {
|
||||
return this.dialect.buildRefreshMaterializedViewQuery(this.config);
|
||||
}
|
||||
|
||||
toSQL(): Query {
|
||||
const { typings: _typings, ...rest } = this.dialect.sqlToQuery(this.getSQL());
|
||||
return rest;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_prepare(name?: string): GelPreparedQuery<
|
||||
PreparedQueryConfig & {
|
||||
execute: GelQueryResultKind<TQueryResult, never>;
|
||||
}
|
||||
> {
|
||||
return tracer.startActiveSpan('drizzle.prepareQuery', () => {
|
||||
return this.session.prepareQuery(this.dialect.sqlToQuery(this.getSQL()), undefined, name, true);
|
||||
});
|
||||
}
|
||||
|
||||
prepare(name: string): GelPreparedQuery<
|
||||
PreparedQueryConfig & {
|
||||
execute: GelQueryResultKind<TQueryResult, never>;
|
||||
}
|
||||
> {
|
||||
return this._prepare(name);
|
||||
}
|
||||
|
||||
execute: ReturnType<this['prepare']>['execute'] = (placeholderValues) => {
|
||||
return tracer.startActiveSpan('drizzle.operation', () => {
|
||||
return this._prepare().execute(placeholderValues);
|
||||
});
|
||||
};
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,425 @@
|
||||
import type { GelColumn } from '~/gel-core/columns/index.ts';
|
||||
import type { GelTable, GelTableWithColumns } from '~/gel-core/table.ts';
|
||||
import type { GelViewBase } from '~/gel-core/view-base.ts';
|
||||
import type { GelViewWithSelection } from '~/gel-core/view.ts';
|
||||
import type {
|
||||
SelectedFields as SelectedFieldsBase,
|
||||
SelectedFieldsFlat as SelectedFieldsFlatBase,
|
||||
SelectedFieldsOrdered as SelectedFieldsOrderedBase,
|
||||
} from '~/operations.ts';
|
||||
import type { TypedQueryBuilder } from '~/query-builders/query-builder.ts';
|
||||
import type {
|
||||
AppendToNullabilityMap,
|
||||
AppendToResult,
|
||||
BuildSubquerySelection,
|
||||
GetSelectTableName,
|
||||
JoinNullability,
|
||||
JoinType,
|
||||
MapColumnsToTableAlias,
|
||||
SelectMode,
|
||||
SelectResult,
|
||||
SetOperator,
|
||||
} from '~/query-builders/select.types.ts';
|
||||
import type { ColumnsSelection, Placeholder, SQL, SQLWrapper, View } from '~/sql/sql.ts';
|
||||
import type { Subquery } from '~/subquery.ts';
|
||||
import type { Table, UpdateTableConfig } from '~/table.ts';
|
||||
import type { Assume, ValidateShape, ValueOrArray } from '~/utils.ts';
|
||||
import type { GelPreparedQuery, PreparedQueryConfig } from '../session.ts';
|
||||
import type { GelSelectBase, GelSelectQueryBuilderBase } from './select.ts';
|
||||
|
||||
export interface GelSelectJoinConfig {
|
||||
on: SQL | undefined;
|
||||
table: GelTable | Subquery | GelViewBase | SQL;
|
||||
alias: string | undefined;
|
||||
joinType: JoinType;
|
||||
lateral?: boolean;
|
||||
}
|
||||
|
||||
export type BuildAliasTable<TTable extends GelTable | View, TAlias extends string> = TTable extends Table
|
||||
? GelTableWithColumns<
|
||||
UpdateTableConfig<TTable['_']['config'], {
|
||||
name: TAlias;
|
||||
columns: MapColumnsToTableAlias<TTable['_']['columns'], TAlias, 'gel'>;
|
||||
}>
|
||||
>
|
||||
: TTable extends View ? GelViewWithSelection<
|
||||
TAlias,
|
||||
TTable['_']['existing'],
|
||||
MapColumnsToTableAlias<TTable['_']['selectedFields'], TAlias, 'gel'>
|
||||
>
|
||||
: never;
|
||||
|
||||
export interface GelSelectConfig {
|
||||
withList?: Subquery[];
|
||||
// Either fields or fieldsFlat must be defined
|
||||
fields: Record<string, unknown>;
|
||||
fieldsFlat?: SelectedFieldsOrdered;
|
||||
where?: SQL;
|
||||
having?: SQL;
|
||||
table: GelTable | Subquery | GelViewBase | SQL;
|
||||
limit?: number | Placeholder;
|
||||
offset?: number | Placeholder;
|
||||
joins?: GelSelectJoinConfig[];
|
||||
orderBy?: (GelColumn | SQL | SQL.Aliased)[];
|
||||
groupBy?: (GelColumn | SQL | SQL.Aliased)[];
|
||||
lockingClause?: {
|
||||
strength: LockStrength;
|
||||
config: LockConfig;
|
||||
};
|
||||
distinct?: boolean | {
|
||||
on: (GelColumn | SQLWrapper)[];
|
||||
};
|
||||
setOperators: {
|
||||
rightSelect: TypedQueryBuilder<any, any>;
|
||||
type: SetOperator;
|
||||
isAll: boolean;
|
||||
orderBy?: (GelColumn | SQL | SQL.Aliased)[];
|
||||
limit?: number | Placeholder;
|
||||
offset?: number | Placeholder;
|
||||
}[];
|
||||
}
|
||||
|
||||
export type GelSelectJoin<
|
||||
T extends AnyGelSelectQueryBuilder,
|
||||
TDynamic extends boolean,
|
||||
TJoinType extends JoinType,
|
||||
TJoinedTable extends GelTable | Subquery | GelViewBase | SQL,
|
||||
TJoinedName extends GetSelectTableName<TJoinedTable> = GetSelectTableName<TJoinedTable>,
|
||||
> = T extends any ? GelSelectWithout<
|
||||
GelSelectKind<
|
||||
T['_']['hkt'],
|
||||
T['_']['tableName'],
|
||||
AppendToResult<
|
||||
T['_']['tableName'],
|
||||
T['_']['selection'],
|
||||
TJoinedName,
|
||||
TJoinedTable extends Table ? TJoinedTable['_']['columns']
|
||||
: TJoinedTable extends Subquery ? Assume<TJoinedTable['_']['selectedFields'], SelectedFields>
|
||||
: never,
|
||||
T['_']['selectMode']
|
||||
>,
|
||||
T['_']['selectMode'] extends 'partial' ? T['_']['selectMode'] : 'multiple',
|
||||
AppendToNullabilityMap<T['_']['nullabilityMap'], TJoinedName, TJoinType>,
|
||||
T['_']['dynamic'],
|
||||
T['_']['excludedMethods']
|
||||
>,
|
||||
TDynamic,
|
||||
T['_']['excludedMethods']
|
||||
>
|
||||
: never;
|
||||
|
||||
export type GelSelectJoinFn<
|
||||
T extends AnyGelSelectQueryBuilder,
|
||||
TDynamic extends boolean,
|
||||
TJoinType extends JoinType,
|
||||
TIsLateral extends boolean,
|
||||
> = <
|
||||
TJoinedTable extends (TIsLateral extends true ? Subquery | SQL : GelTable | Subquery | GelViewBase | SQL),
|
||||
TJoinedName extends GetSelectTableName<TJoinedTable> = GetSelectTableName<TJoinedTable>,
|
||||
>(
|
||||
table: TJoinedTable,
|
||||
on: ((aliases: T['_']['selection']) => SQL | undefined) | SQL | undefined,
|
||||
) => GelSelectJoin<T, TDynamic, TJoinType, TJoinedTable, TJoinedName>;
|
||||
|
||||
export type GelSelectCrossJoinFn<
|
||||
T extends AnyGelSelectQueryBuilder,
|
||||
TDynamic extends boolean,
|
||||
TIsLateral extends boolean,
|
||||
> = <
|
||||
TJoinedTable extends (TIsLateral extends true ? Subquery | SQL : GelTable | Subquery | GelViewBase | SQL),
|
||||
TJoinedName extends GetSelectTableName<TJoinedTable> = GetSelectTableName<TJoinedTable>,
|
||||
>(table: TJoinedTable) => GelSelectJoin<T, TDynamic, 'cross', TJoinedTable, TJoinedName>;
|
||||
|
||||
export type SelectedFieldsFlat = SelectedFieldsFlatBase<GelColumn>;
|
||||
|
||||
export type SelectedFields = SelectedFieldsBase<GelColumn, GelTable>;
|
||||
|
||||
export type SelectedFieldsOrdered = SelectedFieldsOrderedBase<GelColumn>;
|
||||
|
||||
export type LockStrength = 'update' | 'no key update' | 'share' | 'key share';
|
||||
|
||||
export type LockConfig =
|
||||
& {
|
||||
of?: ValueOrArray<GelTable>;
|
||||
}
|
||||
& ({
|
||||
noWait: true;
|
||||
skipLocked?: undefined;
|
||||
} | {
|
||||
noWait?: undefined;
|
||||
skipLocked: true;
|
||||
} | {
|
||||
noWait?: undefined;
|
||||
skipLocked?: undefined;
|
||||
});
|
||||
|
||||
export interface GelSelectHKTBase {
|
||||
tableName: string | undefined;
|
||||
selection: unknown;
|
||||
selectMode: SelectMode;
|
||||
nullabilityMap: unknown;
|
||||
dynamic: boolean;
|
||||
excludedMethods: string;
|
||||
result: unknown;
|
||||
selectedFields: unknown;
|
||||
_type: unknown;
|
||||
}
|
||||
|
||||
export type GelSelectKind<
|
||||
T extends GelSelectHKTBase,
|
||||
TTableName extends string | undefined,
|
||||
TSelection extends ColumnsSelection,
|
||||
TSelectMode extends SelectMode,
|
||||
TNullabilityMap extends Record<string, JoinNullability>,
|
||||
TDynamic extends boolean,
|
||||
TExcludedMethods extends string,
|
||||
TResult = SelectResult<TSelection, TSelectMode, TNullabilityMap>[],
|
||||
TSelectedFields = BuildSubquerySelection<TSelection, TNullabilityMap>,
|
||||
> = (T & {
|
||||
tableName: TTableName;
|
||||
selection: TSelection;
|
||||
selectMode: TSelectMode;
|
||||
nullabilityMap: TNullabilityMap;
|
||||
dynamic: TDynamic;
|
||||
excludedMethods: TExcludedMethods;
|
||||
result: TResult;
|
||||
selectedFields: TSelectedFields;
|
||||
})['_type'];
|
||||
|
||||
export interface GelSelectQueryBuilderHKT extends GelSelectHKTBase {
|
||||
_type: GelSelectQueryBuilderBase<
|
||||
GelSelectQueryBuilderHKT,
|
||||
this['tableName'],
|
||||
Assume<this['selection'], ColumnsSelection>,
|
||||
this['selectMode'],
|
||||
Assume<this['nullabilityMap'], Record<string, JoinNullability>>,
|
||||
this['dynamic'],
|
||||
this['excludedMethods'],
|
||||
Assume<this['result'], any[]>,
|
||||
Assume<this['selectedFields'], ColumnsSelection>
|
||||
>;
|
||||
}
|
||||
|
||||
export interface GelSelectHKT extends GelSelectHKTBase {
|
||||
_type: GelSelectBase<
|
||||
this['tableName'],
|
||||
Assume<this['selection'], ColumnsSelection>,
|
||||
this['selectMode'],
|
||||
Assume<this['nullabilityMap'], Record<string, JoinNullability>>,
|
||||
this['dynamic'],
|
||||
this['excludedMethods'],
|
||||
Assume<this['result'], any[]>,
|
||||
Assume<this['selectedFields'], ColumnsSelection>
|
||||
>;
|
||||
}
|
||||
|
||||
export type CreateGelSelectFromBuilderMode<
|
||||
TBuilderMode extends 'db' | 'qb',
|
||||
TTableName extends string | undefined,
|
||||
TSelection extends ColumnsSelection,
|
||||
TSelectMode extends SelectMode,
|
||||
> = TBuilderMode extends 'db' ? GelSelectBase<TTableName, TSelection, TSelectMode>
|
||||
: GelSelectQueryBuilderBase<GelSelectQueryBuilderHKT, TTableName, TSelection, TSelectMode>;
|
||||
|
||||
export type GelSetOperatorExcludedMethods =
|
||||
| 'leftJoin'
|
||||
| 'rightJoin'
|
||||
| 'innerJoin'
|
||||
| 'fullJoin'
|
||||
| 'where'
|
||||
| 'having'
|
||||
| 'groupBy'
|
||||
| 'for';
|
||||
|
||||
export type GelSelectWithout<
|
||||
T extends AnyGelSelectQueryBuilder,
|
||||
TDynamic extends boolean,
|
||||
K extends keyof T & string,
|
||||
TResetExcluded extends boolean = false,
|
||||
> = TDynamic extends true ? T : Omit<
|
||||
GelSelectKind<
|
||||
T['_']['hkt'],
|
||||
T['_']['tableName'],
|
||||
T['_']['selection'],
|
||||
T['_']['selectMode'],
|
||||
T['_']['nullabilityMap'],
|
||||
TDynamic,
|
||||
TResetExcluded extends true ? K : T['_']['excludedMethods'] | K,
|
||||
T['_']['result'],
|
||||
T['_']['selectedFields']
|
||||
>,
|
||||
TResetExcluded extends true ? K : T['_']['excludedMethods'] | K
|
||||
>;
|
||||
|
||||
export type GelSelectPrepare<T extends AnyGelSelect> = GelPreparedQuery<
|
||||
PreparedQueryConfig & {
|
||||
execute: T['_']['result'];
|
||||
}
|
||||
>;
|
||||
|
||||
export type GelSelectDynamic<T extends AnyGelSelectQueryBuilder> = GelSelectKind<
|
||||
T['_']['hkt'],
|
||||
T['_']['tableName'],
|
||||
T['_']['selection'],
|
||||
T['_']['selectMode'],
|
||||
T['_']['nullabilityMap'],
|
||||
true,
|
||||
never,
|
||||
T['_']['result'],
|
||||
T['_']['selectedFields']
|
||||
>;
|
||||
|
||||
export type GelSelectQueryBuilder<
|
||||
THKT extends GelSelectHKTBase = GelSelectQueryBuilderHKT,
|
||||
TTableName extends string | undefined = string | undefined,
|
||||
TSelection extends ColumnsSelection = ColumnsSelection,
|
||||
TSelectMode extends SelectMode = SelectMode,
|
||||
TNullabilityMap extends Record<string, JoinNullability> = Record<string, JoinNullability>,
|
||||
TResult extends any[] = unknown[],
|
||||
TSelectedFields extends ColumnsSelection = ColumnsSelection,
|
||||
> = GelSelectQueryBuilderBase<
|
||||
THKT,
|
||||
TTableName,
|
||||
TSelection,
|
||||
TSelectMode,
|
||||
TNullabilityMap,
|
||||
true,
|
||||
never,
|
||||
TResult,
|
||||
TSelectedFields
|
||||
>;
|
||||
|
||||
export type AnyGelSelectQueryBuilder = GelSelectQueryBuilderBase<any, any, any, any, any, any, any, any, any>;
|
||||
|
||||
export type AnyGelSetOperatorInterface = GelSetOperatorInterface<any, any, any, any, any, any, any, any>;
|
||||
|
||||
export interface GelSetOperatorInterface<
|
||||
TTableName extends string | undefined,
|
||||
TSelection extends ColumnsSelection,
|
||||
TSelectMode extends SelectMode,
|
||||
TNullabilityMap extends Record<string, JoinNullability> = TTableName extends string ? Record<TTableName, 'not-null'>
|
||||
: {},
|
||||
TDynamic extends boolean = false,
|
||||
TExcludedMethods extends string = never,
|
||||
TResult extends any[] = SelectResult<TSelection, TSelectMode, TNullabilityMap>[],
|
||||
TSelectedFields extends ColumnsSelection = BuildSubquerySelection<TSelection, TNullabilityMap>,
|
||||
> {
|
||||
_: {
|
||||
readonly hkt: GelSelectHKT;
|
||||
readonly tableName: TTableName;
|
||||
readonly selection: TSelection;
|
||||
readonly selectMode: TSelectMode;
|
||||
readonly nullabilityMap: TNullabilityMap;
|
||||
readonly dynamic: TDynamic;
|
||||
readonly excludedMethods: TExcludedMethods;
|
||||
readonly result: TResult;
|
||||
readonly selectedFields: TSelectedFields;
|
||||
};
|
||||
}
|
||||
|
||||
export type GelSetOperatorWithResult<TResult extends any[]> = GelSetOperatorInterface<
|
||||
any,
|
||||
any,
|
||||
any,
|
||||
any,
|
||||
any,
|
||||
any,
|
||||
TResult,
|
||||
any
|
||||
>;
|
||||
|
||||
export type GelSelect<
|
||||
TTableName extends string | undefined = string | undefined,
|
||||
TSelection extends ColumnsSelection = Record<string, any>,
|
||||
TSelectMode extends SelectMode = SelectMode,
|
||||
TNullabilityMap extends Record<string, JoinNullability> = Record<string, JoinNullability>,
|
||||
> = GelSelectBase<TTableName, TSelection, TSelectMode, TNullabilityMap, true, never>;
|
||||
|
||||
export type AnyGelSelect = GelSelectBase<any, any, any, any, any, any, any, any>;
|
||||
|
||||
export type GelSetOperator<
|
||||
TTableName extends string | undefined = string | undefined,
|
||||
TSelection extends ColumnsSelection = Record<string, any>,
|
||||
TSelectMode extends SelectMode = SelectMode,
|
||||
TNullabilityMap extends Record<string, JoinNullability> = Record<string, JoinNullability>,
|
||||
> = GelSelectBase<
|
||||
TTableName,
|
||||
TSelection,
|
||||
TSelectMode,
|
||||
TNullabilityMap,
|
||||
true,
|
||||
GelSetOperatorExcludedMethods
|
||||
>;
|
||||
|
||||
export type SetOperatorRightSelect<
|
||||
TValue extends GelSetOperatorWithResult<TResult>,
|
||||
TResult extends any[],
|
||||
> = TValue extends GelSetOperatorInterface<any, any, any, any, any, any, infer TValueResult, any> ? ValidateShape<
|
||||
TValueResult[number],
|
||||
TResult[number],
|
||||
TypedQueryBuilder<any, TValueResult>
|
||||
>
|
||||
: TValue;
|
||||
|
||||
export type SetOperatorRestSelect<
|
||||
TValue extends readonly GelSetOperatorWithResult<TResult>[],
|
||||
TResult extends any[],
|
||||
> = TValue extends [infer First, ...infer Rest]
|
||||
? First extends GelSetOperatorInterface<any, any, any, any, any, any, infer TValueResult, any>
|
||||
? Rest extends AnyGelSetOperatorInterface[] ? [
|
||||
ValidateShape<TValueResult[number], TResult[number], TypedQueryBuilder<any, TValueResult>>,
|
||||
...SetOperatorRestSelect<Rest, TResult>,
|
||||
]
|
||||
: ValidateShape<TValueResult[number], TResult[number], TypedQueryBuilder<any, TValueResult>[]>
|
||||
: never
|
||||
: TValue;
|
||||
|
||||
export type GelCreateSetOperatorFn = <
|
||||
TTableName extends string | undefined,
|
||||
TSelection extends ColumnsSelection,
|
||||
TSelectMode extends SelectMode,
|
||||
TValue extends GelSetOperatorWithResult<TResult>,
|
||||
TRest extends GelSetOperatorWithResult<TResult>[],
|
||||
TNullabilityMap extends Record<string, JoinNullability> = TTableName extends string ? Record<TTableName, 'not-null'>
|
||||
: {},
|
||||
TDynamic extends boolean = false,
|
||||
TExcludedMethods extends string = never,
|
||||
TResult extends any[] = SelectResult<TSelection, TSelectMode, TNullabilityMap>[],
|
||||
TSelectedFields extends ColumnsSelection = BuildSubquerySelection<TSelection, TNullabilityMap>,
|
||||
>(
|
||||
leftSelect: GelSetOperatorInterface<
|
||||
TTableName,
|
||||
TSelection,
|
||||
TSelectMode,
|
||||
TNullabilityMap,
|
||||
TDynamic,
|
||||
TExcludedMethods,
|
||||
TResult,
|
||||
TSelectedFields
|
||||
>,
|
||||
rightSelect: SetOperatorRightSelect<TValue, TResult>,
|
||||
...restSelects: SetOperatorRestSelect<TRest, TResult>
|
||||
) => GelSelectWithout<
|
||||
GelSelectBase<
|
||||
TTableName,
|
||||
TSelection,
|
||||
TSelectMode,
|
||||
TNullabilityMap,
|
||||
TDynamic,
|
||||
TExcludedMethods,
|
||||
TResult,
|
||||
TSelectedFields
|
||||
>,
|
||||
false,
|
||||
GelSetOperatorExcludedMethods,
|
||||
true
|
||||
>;
|
||||
|
||||
export type GetGelSetOperators = {
|
||||
union: GelCreateSetOperatorFn;
|
||||
intersect: GelCreateSetOperatorFn;
|
||||
except: GelCreateSetOperatorFn;
|
||||
unionAll: GelCreateSetOperatorFn;
|
||||
intersectAll: GelCreateSetOperatorFn;
|
||||
exceptAll: GelCreateSetOperatorFn;
|
||||
};
|
||||
@@ -0,0 +1,561 @@
|
||||
import type { GetColumnData } from '~/column.ts';
|
||||
import { entityKind, is } from '~/entity.ts';
|
||||
import type { GelDialect } from '~/gel-core/dialect.ts';
|
||||
import type {
|
||||
GelPreparedQuery,
|
||||
GelQueryResultHKT,
|
||||
GelQueryResultKind,
|
||||
GelSession,
|
||||
PreparedQueryConfig,
|
||||
} from '~/gel-core/session.ts';
|
||||
import { GelTable } from '~/gel-core/table.ts';
|
||||
import type {
|
||||
AppendToNullabilityMap,
|
||||
AppendToResult,
|
||||
GetSelectTableName,
|
||||
GetSelectTableSelection,
|
||||
JoinNullability,
|
||||
JoinType,
|
||||
SelectMode,
|
||||
SelectResult,
|
||||
} from '~/query-builders/select.types.ts';
|
||||
import { QueryPromise } from '~/query-promise.ts';
|
||||
import type { RunnableQuery } from '~/runnable-query.ts';
|
||||
import { SelectionProxyHandler } from '~/selection-proxy.ts';
|
||||
import { type ColumnsSelection, type Query, SQL, type SQLWrapper } from '~/sql/sql.ts';
|
||||
import { Subquery } from '~/subquery.ts';
|
||||
import { Table } from '~/table.ts';
|
||||
import {
|
||||
type Assume,
|
||||
getTableLikeName,
|
||||
mapUpdateSet,
|
||||
type NeonAuthToken,
|
||||
orderSelectedFields,
|
||||
type UpdateSet,
|
||||
} from '~/utils.ts';
|
||||
import { ViewBaseConfig } from '~/view-common.ts';
|
||||
import type { GelColumn } from '../columns/common.ts';
|
||||
import { extractUsedTable } from '../utils.ts';
|
||||
import type { GelViewBase } from '../view-base.ts';
|
||||
import type { GelSelectJoinConfig, SelectedFields, SelectedFieldsOrdered } from './select.types.ts';
|
||||
|
||||
export interface GelUpdateConfig {
|
||||
where?: SQL | undefined;
|
||||
set: UpdateSet;
|
||||
table: GelTable;
|
||||
from?: GelTable | Subquery | GelViewBase | SQL;
|
||||
joins: GelSelectJoinConfig[];
|
||||
returning?: SelectedFieldsOrdered;
|
||||
withList?: Subquery[];
|
||||
}
|
||||
|
||||
export type GelUpdateSetSource<TTable extends GelTable> =
|
||||
& {
|
||||
[Key in keyof TTable['$inferInsert']]?:
|
||||
| GetColumnData<TTable['_']['columns'][Key]>
|
||||
| SQL
|
||||
| GelColumn;
|
||||
}
|
||||
& {};
|
||||
|
||||
export class GelUpdateBuilder<TTable extends GelTable, TQueryResult extends GelQueryResultHKT> {
|
||||
static readonly [entityKind]: string = 'GelUpdateBuilder';
|
||||
|
||||
declare readonly _: {
|
||||
readonly table: TTable;
|
||||
};
|
||||
|
||||
constructor(
|
||||
private table: TTable,
|
||||
private session: GelSession,
|
||||
private dialect: GelDialect,
|
||||
private withList?: Subquery[],
|
||||
) {}
|
||||
|
||||
private authToken?: NeonAuthToken;
|
||||
setToken(token: NeonAuthToken) {
|
||||
this.authToken = token;
|
||||
return this;
|
||||
}
|
||||
|
||||
set(
|
||||
values: GelUpdateSetSource<TTable>,
|
||||
): GelUpdateWithout<GelUpdateBase<TTable, TQueryResult>, false, 'leftJoin' | 'rightJoin' | 'innerJoin' | 'fullJoin'> {
|
||||
return new GelUpdateBase<TTable, TQueryResult>(
|
||||
this.table,
|
||||
mapUpdateSet(this.table, values),
|
||||
this.session,
|
||||
this.dialect,
|
||||
this.withList,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export type GelUpdateWithout<
|
||||
T extends AnyGelUpdate,
|
||||
TDynamic extends boolean,
|
||||
K extends keyof T & string,
|
||||
> = TDynamic extends true ? T : Omit<
|
||||
GelUpdateBase<
|
||||
T['_']['table'],
|
||||
T['_']['queryResult'],
|
||||
T['_']['from'],
|
||||
T['_']['returning'],
|
||||
T['_']['nullabilityMap'],
|
||||
T['_']['joins'],
|
||||
TDynamic,
|
||||
T['_']['excludedMethods'] | K
|
||||
>,
|
||||
T['_']['excludedMethods'] | K
|
||||
>;
|
||||
|
||||
export type GelUpdateWithJoins<
|
||||
T extends AnyGelUpdate,
|
||||
TDynamic extends boolean,
|
||||
TFrom extends GelTable | Subquery | GelViewBase | SQL,
|
||||
> = TDynamic extends true ? T : Omit<
|
||||
GelUpdateBase<
|
||||
T['_']['table'],
|
||||
T['_']['queryResult'],
|
||||
TFrom,
|
||||
T['_']['returning'],
|
||||
AppendToNullabilityMap<T['_']['nullabilityMap'], GetSelectTableName<TFrom>, 'inner'>,
|
||||
[...T['_']['joins'], {
|
||||
name: GetSelectTableName<TFrom>;
|
||||
joinType: 'inner';
|
||||
table: TFrom;
|
||||
}],
|
||||
TDynamic,
|
||||
Exclude<T['_']['excludedMethods'] | 'from', 'leftJoin' | 'rightJoin' | 'innerJoin' | 'fullJoin'>
|
||||
>,
|
||||
Exclude<T['_']['excludedMethods'] | 'from', 'leftJoin' | 'rightJoin' | 'innerJoin' | 'fullJoin'>
|
||||
>;
|
||||
|
||||
export type GelUpdateJoinFn<
|
||||
T extends AnyGelUpdate,
|
||||
TDynamic extends boolean,
|
||||
TJoinType extends JoinType,
|
||||
> = <
|
||||
TJoinedTable extends GelTable | Subquery | GelViewBase | SQL,
|
||||
>(
|
||||
table: TJoinedTable,
|
||||
on:
|
||||
| (
|
||||
(
|
||||
updateTable: T['_']['table']['_']['columns'],
|
||||
from: T['_']['from'] extends GelTable ? T['_']['from']['_']['columns']
|
||||
: T['_']['from'] extends Subquery | GelViewBase ? T['_']['from']['_']['selectedFields']
|
||||
: never,
|
||||
) => SQL | undefined
|
||||
)
|
||||
| SQL
|
||||
| undefined,
|
||||
) => GelUpdateJoin<T, TDynamic, TJoinType, TJoinedTable>;
|
||||
|
||||
export type GelUpdateJoin<
|
||||
T extends AnyGelUpdate,
|
||||
TDynamic extends boolean,
|
||||
TJoinType extends JoinType,
|
||||
TJoinedTable extends GelTable | Subquery | GelViewBase | SQL,
|
||||
> = TDynamic extends true ? T : GelUpdateBase<
|
||||
T['_']['table'],
|
||||
T['_']['queryResult'],
|
||||
T['_']['from'],
|
||||
T['_']['returning'],
|
||||
AppendToNullabilityMap<T['_']['nullabilityMap'], GetSelectTableName<TJoinedTable>, TJoinType>,
|
||||
[...T['_']['joins'], {
|
||||
name: GetSelectTableName<TJoinedTable>;
|
||||
joinType: TJoinType;
|
||||
table: TJoinedTable;
|
||||
}],
|
||||
TDynamic,
|
||||
T['_']['excludedMethods']
|
||||
>;
|
||||
|
||||
type Join = {
|
||||
name: string | undefined;
|
||||
joinType: JoinType;
|
||||
table: GelTable | Subquery | GelViewBase | SQL;
|
||||
};
|
||||
|
||||
type AccumulateToResult<
|
||||
T extends AnyGelUpdate,
|
||||
TSelectMode extends SelectMode,
|
||||
TJoins extends Join[],
|
||||
TSelectedFields extends ColumnsSelection,
|
||||
> = TJoins extends [infer TJoin extends Join, ...infer TRest extends Join[]] ? AccumulateToResult<
|
||||
T,
|
||||
TSelectMode extends 'partial' ? TSelectMode : 'multiple',
|
||||
TRest,
|
||||
AppendToResult<
|
||||
T['_']['table']['_']['name'],
|
||||
TSelectedFields,
|
||||
TJoin['name'],
|
||||
TJoin['table'] extends Table ? TJoin['table']['_']['columns']
|
||||
: TJoin['table'] extends Subquery ? Assume<TJoin['table']['_']['selectedFields'], SelectedFields>
|
||||
: never,
|
||||
TSelectMode extends 'partial' ? TSelectMode : 'multiple'
|
||||
>
|
||||
>
|
||||
: TSelectedFields;
|
||||
|
||||
export type GelUpdateReturningAll<T extends AnyGelUpdate, TDynamic extends boolean> = GelUpdateWithout<
|
||||
GelUpdateBase<
|
||||
T['_']['table'],
|
||||
T['_']['queryResult'],
|
||||
T['_']['from'],
|
||||
SelectResult<
|
||||
AccumulateToResult<
|
||||
T,
|
||||
'single',
|
||||
T['_']['joins'],
|
||||
GetSelectTableSelection<T['_']['table']>
|
||||
>,
|
||||
'partial',
|
||||
T['_']['nullabilityMap']
|
||||
>,
|
||||
T['_']['nullabilityMap'],
|
||||
T['_']['joins'],
|
||||
TDynamic,
|
||||
T['_']['excludedMethods']
|
||||
>,
|
||||
TDynamic,
|
||||
'returning'
|
||||
>;
|
||||
|
||||
export type GelUpdateReturning<
|
||||
T extends AnyGelUpdate,
|
||||
TDynamic extends boolean,
|
||||
TSelectedFields extends SelectedFields,
|
||||
> = GelUpdateWithout<
|
||||
GelUpdateBase<
|
||||
T['_']['table'],
|
||||
T['_']['queryResult'],
|
||||
T['_']['from'],
|
||||
SelectResult<
|
||||
AccumulateToResult<
|
||||
T,
|
||||
'partial',
|
||||
T['_']['joins'],
|
||||
TSelectedFields
|
||||
>,
|
||||
'partial',
|
||||
T['_']['nullabilityMap']
|
||||
>,
|
||||
T['_']['nullabilityMap'],
|
||||
T['_']['joins'],
|
||||
TDynamic,
|
||||
T['_']['excludedMethods']
|
||||
>,
|
||||
TDynamic,
|
||||
'returning'
|
||||
>;
|
||||
|
||||
export type GelUpdatePrepare<T extends AnyGelUpdate> = GelPreparedQuery<
|
||||
PreparedQueryConfig & {
|
||||
execute: T['_']['returning'] extends undefined ? GelQueryResultKind<T['_']['queryResult'], never>
|
||||
: T['_']['returning'][];
|
||||
}
|
||||
>;
|
||||
|
||||
export type GelUpdateDynamic<T extends AnyGelUpdate> = GelUpdate<
|
||||
T['_']['table'],
|
||||
T['_']['queryResult'],
|
||||
T['_']['from'],
|
||||
T['_']['returning'],
|
||||
T['_']['nullabilityMap']
|
||||
>;
|
||||
|
||||
export type GelUpdate<
|
||||
TTable extends GelTable = GelTable,
|
||||
TQueryResult extends GelQueryResultHKT = GelQueryResultHKT,
|
||||
TFrom extends GelTable | Subquery | GelViewBase | SQL | undefined = undefined,
|
||||
TReturning extends Record<string, unknown> | undefined = Record<string, unknown> | undefined,
|
||||
TNullabilityMap extends Record<string, JoinNullability> = Record<TTable['_']['name'], 'not-null'>,
|
||||
TJoins extends Join[] = [],
|
||||
> = GelUpdateBase<TTable, TQueryResult, TFrom, TReturning, TNullabilityMap, TJoins, true, never>;
|
||||
|
||||
export type AnyGelUpdate = GelUpdateBase<any, any, any, any, any, any, any, any>;
|
||||
|
||||
export interface GelUpdateBase<
|
||||
TTable extends GelTable,
|
||||
TQueryResult extends GelQueryResultHKT,
|
||||
TFrom extends GelTable | Subquery | GelViewBase | SQL | undefined = undefined,
|
||||
TReturning extends Record<string, unknown> | undefined = undefined,
|
||||
TNullabilityMap extends Record<string, JoinNullability> = Record<TTable['_']['name'], 'not-null'>,
|
||||
TJoins extends Join[] = [],
|
||||
TDynamic extends boolean = false,
|
||||
TExcludedMethods extends string = never,
|
||||
> extends
|
||||
QueryPromise<TReturning extends undefined ? GelQueryResultKind<TQueryResult, never> : TReturning[]>,
|
||||
RunnableQuery<TReturning extends undefined ? GelQueryResultKind<TQueryResult, never> : TReturning[], 'gel'>,
|
||||
SQLWrapper
|
||||
{
|
||||
readonly _: {
|
||||
readonly dialect: 'gel';
|
||||
readonly table: TTable;
|
||||
readonly joins: TJoins;
|
||||
readonly nullabilityMap: TNullabilityMap;
|
||||
readonly queryResult: TQueryResult;
|
||||
readonly from: TFrom;
|
||||
readonly returning: TReturning;
|
||||
readonly dynamic: TDynamic;
|
||||
readonly excludedMethods: TExcludedMethods;
|
||||
readonly result: TReturning extends undefined ? GelQueryResultKind<TQueryResult, never> : TReturning[];
|
||||
};
|
||||
}
|
||||
|
||||
export class GelUpdateBase<
|
||||
TTable extends GelTable,
|
||||
TQueryResult extends GelQueryResultHKT,
|
||||
TFrom extends GelTable | Subquery | GelViewBase | SQL | undefined = undefined,
|
||||
TReturning extends Record<string, unknown> | undefined = undefined,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
TNullabilityMap extends Record<string, JoinNullability> = Record<TTable['_']['name'], 'not-null'>,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
TJoins extends Join[] = [],
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
TDynamic extends boolean = false,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
TExcludedMethods extends string = never,
|
||||
> extends QueryPromise<TReturning extends undefined ? GelQueryResultKind<TQueryResult, never> : TReturning[]>
|
||||
implements
|
||||
RunnableQuery<TReturning extends undefined ? GelQueryResultKind<TQueryResult, never> : TReturning[], 'gel'>,
|
||||
SQLWrapper
|
||||
{
|
||||
static override readonly [entityKind]: string = 'GelUpdate';
|
||||
|
||||
private config: GelUpdateConfig;
|
||||
private tableName: string | undefined;
|
||||
private joinsNotNullableMap: Record<string, boolean>;
|
||||
|
||||
constructor(
|
||||
table: TTable,
|
||||
set: UpdateSet,
|
||||
private session: GelSession,
|
||||
private dialect: GelDialect,
|
||||
withList?: Subquery[],
|
||||
) {
|
||||
super();
|
||||
this.config = { set, table, withList, joins: [] };
|
||||
this.tableName = getTableLikeName(table);
|
||||
this.joinsNotNullableMap = typeof this.tableName === 'string' ? { [this.tableName]: true } : {};
|
||||
}
|
||||
|
||||
from<TFrom extends GelTable | Subquery | GelViewBase | SQL>(
|
||||
source: TFrom,
|
||||
): GelUpdateWithJoins<this, TDynamic, TFrom> {
|
||||
const tableName = getTableLikeName(source);
|
||||
if (typeof tableName === 'string') {
|
||||
this.joinsNotNullableMap[tableName] = true;
|
||||
}
|
||||
this.config.from = source;
|
||||
return this as any;
|
||||
}
|
||||
|
||||
private getTableLikeFields(table: GelTable | Subquery | GelViewBase): Record<string, unknown> {
|
||||
if (is(table, GelTable)) {
|
||||
return table[Table.Symbol.Columns];
|
||||
} else if (is(table, Subquery)) {
|
||||
return table._.selectedFields;
|
||||
}
|
||||
return table[ViewBaseConfig].selectedFields;
|
||||
}
|
||||
|
||||
private createJoin<TJoinType extends JoinType>(
|
||||
joinType: TJoinType,
|
||||
): GelUpdateJoinFn<this, TDynamic, TJoinType> {
|
||||
return ((
|
||||
table: GelTable | Subquery | GelViewBase | SQL,
|
||||
on: ((updateTable: TTable, from: TFrom) => SQL | undefined) | SQL | undefined,
|
||||
) => {
|
||||
const tableName = getTableLikeName(table);
|
||||
|
||||
if (typeof tableName === 'string' && this.config.joins.some((join) => join.alias === tableName)) {
|
||||
throw new Error(`Alias "${tableName}" is already used in this query`);
|
||||
}
|
||||
|
||||
if (typeof on === 'function') {
|
||||
const from = this.config.from && !is(this.config.from, SQL)
|
||||
? this.getTableLikeFields(this.config.from)
|
||||
: undefined;
|
||||
on = on(
|
||||
new Proxy(
|
||||
this.config.table[Table.Symbol.Columns],
|
||||
new SelectionProxyHandler({ sqlAliasedBehavior: 'sql', sqlBehavior: 'sql' }),
|
||||
) as any,
|
||||
from && new Proxy(
|
||||
from,
|
||||
new SelectionProxyHandler({ sqlAliasedBehavior: 'sql', sqlBehavior: 'sql' }),
|
||||
) as any,
|
||||
);
|
||||
}
|
||||
|
||||
this.config.joins.push({ on, table, joinType, alias: tableName });
|
||||
|
||||
if (typeof tableName === 'string') {
|
||||
switch (joinType) {
|
||||
case 'left': {
|
||||
this.joinsNotNullableMap[tableName] = false;
|
||||
break;
|
||||
}
|
||||
case 'right': {
|
||||
this.joinsNotNullableMap = Object.fromEntries(
|
||||
Object.entries(this.joinsNotNullableMap).map(([key]) => [key, false]),
|
||||
);
|
||||
this.joinsNotNullableMap[tableName] = true;
|
||||
break;
|
||||
}
|
||||
case 'inner': {
|
||||
this.joinsNotNullableMap[tableName] = true;
|
||||
break;
|
||||
}
|
||||
case 'full': {
|
||||
this.joinsNotNullableMap = Object.fromEntries(
|
||||
Object.entries(this.joinsNotNullableMap).map(([key]) => [key, false]),
|
||||
);
|
||||
this.joinsNotNullableMap[tableName] = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return this as any;
|
||||
}) as any;
|
||||
}
|
||||
|
||||
leftJoin = this.createJoin('left');
|
||||
|
||||
rightJoin = this.createJoin('right');
|
||||
|
||||
innerJoin = this.createJoin('inner');
|
||||
|
||||
fullJoin = this.createJoin('full');
|
||||
|
||||
/**
|
||||
* Adds a 'where' clause to the query.
|
||||
*
|
||||
* Calling this method will update only those rows that fulfill a specified condition.
|
||||
*
|
||||
* See docs: {@link https://orm.drizzle.team/docs/update}
|
||||
*
|
||||
* @param where the 'where' clause.
|
||||
*
|
||||
* @example
|
||||
* You can use conditional operators and `sql function` to filter the rows to be updated.
|
||||
*
|
||||
* ```ts
|
||||
* // Update all cars with green color
|
||||
* await db.update(cars).set({ color: 'red' })
|
||||
* .where(eq(cars.color, 'green'));
|
||||
* // or
|
||||
* await db.update(cars).set({ color: 'red' })
|
||||
* .where(sql`${cars.color} = 'green'`)
|
||||
* ```
|
||||
*
|
||||
* You can logically combine conditional operators with `and()` and `or()` operators:
|
||||
*
|
||||
* ```ts
|
||||
* // Update all BMW cars with a green color
|
||||
* await db.update(cars).set({ color: 'red' })
|
||||
* .where(and(eq(cars.color, 'green'), eq(cars.brand, 'BMW')));
|
||||
*
|
||||
* // Update all cars with the green or blue color
|
||||
* await db.update(cars).set({ color: 'red' })
|
||||
* .where(or(eq(cars.color, 'green'), eq(cars.color, 'blue')));
|
||||
* ```
|
||||
*/
|
||||
where(where: SQL | undefined): GelUpdateWithout<this, TDynamic, 'where'> {
|
||||
this.config.where = where;
|
||||
return this as any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a `returning` clause to the query.
|
||||
*
|
||||
* Calling this method will return the specified fields of the updated rows. If no fields are specified, all fields will be returned.
|
||||
*
|
||||
* See docs: {@link https://orm.drizzle.team/docs/update#update-with-returning}
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // Update all cars with the green color and return all fields
|
||||
* const updatedCars: Car[] = await db.update(cars)
|
||||
* .set({ color: 'red' })
|
||||
* .where(eq(cars.color, 'green'))
|
||||
* .returning();
|
||||
*
|
||||
* // Update all cars with the green color and return only their id and brand fields
|
||||
* const updatedCarsIdsAndBrands: { id: number, brand: string }[] = await db.update(cars)
|
||||
* .set({ color: 'red' })
|
||||
* .where(eq(cars.color, 'green'))
|
||||
* .returning({ id: cars.id, brand: cars.brand });
|
||||
* ```
|
||||
*/
|
||||
returning(): GelUpdateReturningAll<this, TDynamic>;
|
||||
returning<TSelectedFields extends SelectedFields>(
|
||||
fields: TSelectedFields,
|
||||
): GelUpdateReturning<this, TDynamic, TSelectedFields>;
|
||||
returning(
|
||||
fields?: SelectedFields,
|
||||
): GelUpdateWithout<AnyGelUpdate, TDynamic, 'returning'> {
|
||||
if (!fields) {
|
||||
fields = Object.assign({}, this.config.table[Table.Symbol.Columns]);
|
||||
|
||||
if (this.config.from) {
|
||||
const tableName = getTableLikeName(this.config.from);
|
||||
|
||||
if (typeof tableName === 'string' && this.config.from && !is(this.config.from, SQL)) {
|
||||
const fromFields = this.getTableLikeFields(this.config.from);
|
||||
fields[tableName] = fromFields as any;
|
||||
}
|
||||
|
||||
for (const join of this.config.joins) {
|
||||
const tableName = getTableLikeName(join.table);
|
||||
|
||||
if (typeof tableName === 'string' && !is(join.table, SQL)) {
|
||||
const fromFields = this.getTableLikeFields(join.table);
|
||||
fields[tableName] = fromFields as any;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.config.returning = orderSelectedFields<GelColumn>(fields);
|
||||
return this as any;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
getSQL(): SQL {
|
||||
return this.dialect.buildUpdateQuery(this.config);
|
||||
}
|
||||
|
||||
toSQL(): Query {
|
||||
const { typings: _typings, ...rest } = this.dialect.sqlToQuery(this.getSQL());
|
||||
return rest;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_prepare(name?: string): GelUpdatePrepare<this> {
|
||||
const query = this.session.prepareQuery<
|
||||
PreparedQueryConfig & { execute: TReturning[] }
|
||||
>(this.dialect.sqlToQuery(this.getSQL()), this.config.returning, name, true, undefined, {
|
||||
type: 'update',
|
||||
tables: extractUsedTable(this.config.table),
|
||||
});
|
||||
query.joinsNotNullableMap = this.joinsNotNullableMap;
|
||||
return query;
|
||||
}
|
||||
|
||||
prepare(name: string): GelUpdatePrepare<this> {
|
||||
return this._prepare(name);
|
||||
}
|
||||
|
||||
override execute: ReturnType<this['prepare']>['execute'] = (placeholderValues) => {
|
||||
return this._prepare().execute(placeholderValues);
|
||||
};
|
||||
|
||||
$dynamic(): GelUpdateDynamic<this> {
|
||||
return this as any;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { entityKind } from '~/entity.ts';
|
||||
|
||||
export interface GelRoleConfig {
|
||||
createDb?: boolean;
|
||||
createRole?: boolean;
|
||||
inherit?: boolean;
|
||||
}
|
||||
|
||||
export class GelRole implements GelRoleConfig {
|
||||
static readonly [entityKind]: string = 'GelRole';
|
||||
|
||||
/** @internal */
|
||||
_existing?: boolean;
|
||||
|
||||
/** @internal */
|
||||
readonly createDb: GelRoleConfig['createDb'];
|
||||
/** @internal */
|
||||
readonly createRole: GelRoleConfig['createRole'];
|
||||
/** @internal */
|
||||
readonly inherit: GelRoleConfig['inherit'];
|
||||
|
||||
constructor(
|
||||
readonly name: string,
|
||||
config?: GelRoleConfig,
|
||||
) {
|
||||
if (config) {
|
||||
this.createDb = config.createDb;
|
||||
this.createRole = config.createRole;
|
||||
this.inherit = config.inherit;
|
||||
}
|
||||
}
|
||||
|
||||
existing(): this {
|
||||
this._existing = true;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
export function gelRole(name: string, config?: GelRoleConfig) {
|
||||
return new GelRole(name, config);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { entityKind, is } from '~/entity.ts';
|
||||
import { SQL, sql, type SQLWrapper } from '~/sql/sql.ts';
|
||||
import type { gelSequence } from './sequence.ts';
|
||||
import { gelSequenceWithSchema } from './sequence.ts';
|
||||
import { type GelTableFn, gelTableWithSchema } from './table.ts';
|
||||
// import type { gelMaterializedView, gelView } from './view.ts';
|
||||
// import { gelMaterializedViewWithSchema, gelViewWithSchema } from './view.ts';
|
||||
|
||||
export class GelSchema<TName extends string = string> implements SQLWrapper {
|
||||
static readonly [entityKind]: string = 'GelSchema';
|
||||
constructor(
|
||||
public readonly schemaName: TName,
|
||||
) {}
|
||||
|
||||
table: GelTableFn<TName> = ((name, columns, extraConfig) => {
|
||||
return gelTableWithSchema(name, columns, extraConfig, this.schemaName);
|
||||
});
|
||||
|
||||
// view = ((name, columns) => {
|
||||
// return gelViewWithSchema(name, columns, this.schemaName);
|
||||
// }) as typeof gelView;
|
||||
|
||||
// materializedView = ((name, columns) => {
|
||||
// return gelMaterializedViewWithSchema(name, columns, this.schemaName);
|
||||
// }) as typeof gelMaterializedView;
|
||||
|
||||
// enum: typeof gelEnum = ((name, values) => {
|
||||
// return gelEnumWithSchema(name, values, this.schemaName);
|
||||
// });
|
||||
|
||||
sequence: typeof gelSequence = ((name, options) => {
|
||||
return gelSequenceWithSchema(name, options, this.schemaName);
|
||||
});
|
||||
|
||||
getSQL(): SQL {
|
||||
return new SQL([sql.identifier(this.schemaName)]);
|
||||
}
|
||||
|
||||
shouldOmitSQLParens(): boolean {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export function isGelSchema(obj: unknown): obj is GelSchema {
|
||||
return is(obj, GelSchema);
|
||||
}
|
||||
|
||||
export function gelSchema<T extends string>(name: T) {
|
||||
if (name === 'public') {
|
||||
throw new Error(
|
||||
`You can't specify 'public' as schema name. Postgres is using public schema by default. If you want to use 'public' schema, just use GelTable() instead of creating a schema`,
|
||||
);
|
||||
}
|
||||
|
||||
return new GelSchema(name);
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { entityKind, is } from '~/entity.ts';
|
||||
|
||||
export type GelSequenceOptions = {
|
||||
increment?: number | string;
|
||||
minValue?: number | string;
|
||||
maxValue?: number | string;
|
||||
startWith?: number | string;
|
||||
cache?: number | string;
|
||||
cycle?: boolean;
|
||||
};
|
||||
|
||||
export class GelSequence {
|
||||
static readonly [entityKind]: string = 'GelSequence';
|
||||
|
||||
constructor(
|
||||
public readonly seqName: string | undefined,
|
||||
public readonly seqOptions: GelSequenceOptions | undefined,
|
||||
public readonly schema: string | undefined,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
export function gelSequence(
|
||||
name: string,
|
||||
options?: GelSequenceOptions,
|
||||
): GelSequence {
|
||||
return gelSequenceWithSchema(name, options, undefined);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function gelSequenceWithSchema(
|
||||
name: string,
|
||||
options?: GelSequenceOptions,
|
||||
schema?: string,
|
||||
): GelSequence {
|
||||
return new GelSequence(name, options, schema);
|
||||
}
|
||||
|
||||
export function isGelSequence(obj: unknown): obj is GelSequence {
|
||||
return is(obj, GelSequence);
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
import { type Cache, hashQuery, NoopCache } from '~/cache/core/cache.ts';
|
||||
import type { WithCacheConfig } from '~/cache/core/types.ts';
|
||||
import { entityKind, is } from '~/entity.ts';
|
||||
import { DrizzleQueryError, TransactionRollbackError } from '~/errors.ts';
|
||||
import type { TablesRelationalConfig } from '~/relations.ts';
|
||||
import type { PreparedQuery } from '~/session.ts';
|
||||
import type { Query, SQL } from '~/sql/index.ts';
|
||||
import { tracer } from '~/tracing.ts';
|
||||
import type { NeonAuthToken } from '~/utils.ts';
|
||||
import { GelDatabase } from './db.ts';
|
||||
import type { GelDialect } from './dialect.ts';
|
||||
import type { SelectedFieldsOrdered } from './query-builders/select.types.ts';
|
||||
|
||||
export interface PreparedQueryConfig {
|
||||
execute: unknown;
|
||||
all: unknown;
|
||||
values: unknown;
|
||||
}
|
||||
|
||||
export abstract class GelPreparedQuery<T extends PreparedQueryConfig> implements PreparedQuery {
|
||||
constructor(
|
||||
protected query: Query,
|
||||
private cache?: Cache,
|
||||
// per query related metadata
|
||||
private queryMetadata?: {
|
||||
type: 'select' | 'update' | 'delete' | 'insert';
|
||||
tables: string[];
|
||||
} | undefined,
|
||||
// config that was passed through $withCache
|
||||
private cacheConfig?: WithCacheConfig,
|
||||
) {
|
||||
// it means that no $withCache options were passed and it should be just enabled
|
||||
if (cache && cache.strategy() === 'all' && cacheConfig === undefined) {
|
||||
this.cacheConfig = { enable: true, autoInvalidate: true };
|
||||
}
|
||||
if (!this.cacheConfig?.enable) {
|
||||
this.cacheConfig = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
protected async queryWithCache<T>(
|
||||
queryString: string,
|
||||
params: any[],
|
||||
query: () => Promise<T>,
|
||||
): Promise<T> {
|
||||
if (this.cache === undefined || is(this.cache, NoopCache) || this.queryMetadata === undefined) {
|
||||
try {
|
||||
return await query();
|
||||
} catch (e) {
|
||||
throw new DrizzleQueryError(queryString, params, e as Error);
|
||||
}
|
||||
}
|
||||
|
||||
// don't do any mutations, if globally is false
|
||||
if (this.cacheConfig && !this.cacheConfig.enable) {
|
||||
try {
|
||||
return await query();
|
||||
} catch (e) {
|
||||
throw new DrizzleQueryError(queryString, params, e as Error);
|
||||
}
|
||||
}
|
||||
|
||||
// For mutate queries, we should query the database, wait for a response, and then perform invalidation
|
||||
if (
|
||||
(
|
||||
this.queryMetadata.type === 'insert' || this.queryMetadata.type === 'update'
|
||||
|| this.queryMetadata.type === 'delete'
|
||||
) && this.queryMetadata.tables.length > 0
|
||||
) {
|
||||
try {
|
||||
const [res] = await Promise.all([
|
||||
query(),
|
||||
this.cache.onMutate({ tables: this.queryMetadata.tables }),
|
||||
]);
|
||||
return res;
|
||||
} catch (e) {
|
||||
throw new DrizzleQueryError(queryString, params, e as Error);
|
||||
}
|
||||
}
|
||||
|
||||
// don't do any reads if globally disabled
|
||||
if (!this.cacheConfig) {
|
||||
try {
|
||||
return await query();
|
||||
} catch (e) {
|
||||
throw new DrizzleQueryError(queryString, params, e as Error);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.queryMetadata.type === 'select') {
|
||||
const fromCache = await this.cache.get(
|
||||
this.cacheConfig.tag ?? await hashQuery(queryString, params),
|
||||
this.queryMetadata.tables,
|
||||
this.cacheConfig.tag !== undefined,
|
||||
this.cacheConfig.autoInvalidate,
|
||||
);
|
||||
if (fromCache === undefined) {
|
||||
let result;
|
||||
try {
|
||||
result = await query();
|
||||
} catch (e) {
|
||||
throw new DrizzleQueryError(queryString, params, e as Error);
|
||||
}
|
||||
|
||||
// put actual key
|
||||
await this.cache.put(
|
||||
this.cacheConfig.tag ?? await hashQuery(queryString, params),
|
||||
result,
|
||||
// make sure we send tables that were used in a query only if user wants to invalidate it on each write
|
||||
this.cacheConfig.autoInvalidate ? this.queryMetadata.tables : [],
|
||||
this.cacheConfig.tag !== undefined,
|
||||
this.cacheConfig.config,
|
||||
);
|
||||
// put flag if we should invalidate or not
|
||||
return result;
|
||||
}
|
||||
|
||||
return fromCache as unknown as T;
|
||||
}
|
||||
try {
|
||||
return await query();
|
||||
} catch (e) {
|
||||
throw new DrizzleQueryError(queryString, params, e as Error);
|
||||
}
|
||||
}
|
||||
|
||||
protected authToken?: NeonAuthToken;
|
||||
|
||||
getQuery(): Query {
|
||||
return this.query;
|
||||
}
|
||||
|
||||
mapResult(response: unknown, _isFromBatch?: boolean): unknown {
|
||||
return response;
|
||||
}
|
||||
|
||||
static readonly [entityKind]: string = 'GelPreparedQuery';
|
||||
|
||||
/** @internal */
|
||||
joinsNotNullableMap?: Record<string, boolean>;
|
||||
|
||||
abstract execute(placeholderValues?: Record<string, unknown>): Promise<T['execute']>;
|
||||
|
||||
/** @internal */
|
||||
abstract all(placeholderValues?: Record<string, unknown>): Promise<T['all']>;
|
||||
|
||||
/** @internal */
|
||||
abstract isResponseInArrayMode(): boolean;
|
||||
}
|
||||
|
||||
export abstract class GelSession<
|
||||
TQueryResult extends GelQueryResultHKT = any, // TO
|
||||
TFullSchema extends Record<string, unknown> = Record<string, never>,
|
||||
TSchema extends TablesRelationalConfig = Record<string, never>,
|
||||
> {
|
||||
static readonly [entityKind]: string = 'GelSession';
|
||||
|
||||
constructor(protected dialect: GelDialect) {}
|
||||
|
||||
abstract prepareQuery<T extends PreparedQueryConfig = PreparedQueryConfig>(
|
||||
query: Query,
|
||||
fields: SelectedFieldsOrdered | undefined,
|
||||
name: string | undefined,
|
||||
isResponseInArrayMode: boolean,
|
||||
customResultMapper?: (rows: unknown[][], mapColumnValue?: (value: unknown) => unknown) => T['execute'],
|
||||
queryMetadata?: {
|
||||
type: 'select' | 'update' | 'delete' | 'insert';
|
||||
tables: string[];
|
||||
},
|
||||
cacheConfig?: WithCacheConfig,
|
||||
): GelPreparedQuery<T>;
|
||||
|
||||
execute<T>(query: SQL): Promise<T> {
|
||||
return tracer.startActiveSpan('drizzle.operation', () => {
|
||||
const prepared = tracer.startActiveSpan('drizzle.prepareQuery', () => {
|
||||
return this.prepareQuery<PreparedQueryConfig & { execute: T }>(
|
||||
this.dialect.sqlToQuery(query),
|
||||
undefined,
|
||||
undefined,
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
return prepared.execute(undefined);
|
||||
});
|
||||
}
|
||||
|
||||
all<T = unknown>(query: SQL): Promise<T[]> {
|
||||
return this.prepareQuery<PreparedQueryConfig & { all: T[] }>(
|
||||
this.dialect.sqlToQuery(query),
|
||||
undefined,
|
||||
undefined,
|
||||
false,
|
||||
).all();
|
||||
}
|
||||
|
||||
async count(sql: SQL): Promise<number> {
|
||||
const res = await this.execute<[{ count: string }]>(sql);
|
||||
|
||||
return Number(
|
||||
res[0]['count'],
|
||||
);
|
||||
}
|
||||
|
||||
abstract transaction<T>(
|
||||
transaction: (tx: GelTransaction<TQueryResult, TFullSchema, TSchema>) => Promise<T>,
|
||||
): Promise<T>;
|
||||
}
|
||||
|
||||
export abstract class GelTransaction<
|
||||
TQueryResult extends GelQueryResultHKT,
|
||||
TFullSchema extends Record<string, unknown> = Record<string, never>,
|
||||
TSchema extends TablesRelationalConfig = Record<string, never>,
|
||||
> extends GelDatabase<TQueryResult, TFullSchema, TSchema> {
|
||||
static override readonly [entityKind]: string = 'GelTransaction';
|
||||
|
||||
constructor(
|
||||
dialect: GelDialect,
|
||||
session: GelSession<any, any, any>,
|
||||
protected schema: {
|
||||
fullSchema: Record<string, unknown>;
|
||||
schema: TSchema;
|
||||
tableNamesMap: Record<string, string>;
|
||||
} | undefined,
|
||||
) {
|
||||
super(dialect, session, schema);
|
||||
}
|
||||
|
||||
rollback(): never {
|
||||
throw new TransactionRollbackError();
|
||||
}
|
||||
|
||||
abstract override transaction<T>(
|
||||
transaction: (tx: GelTransaction<TQueryResult, TFullSchema, TSchema>) => Promise<T>,
|
||||
): Promise<T>;
|
||||
}
|
||||
|
||||
export interface GelQueryResultHKT {
|
||||
readonly $brand: 'GelQueryResultHKT';
|
||||
readonly row: unknown;
|
||||
readonly type: unknown;
|
||||
}
|
||||
|
||||
export type GelQueryResultKind<TKind extends GelQueryResultHKT, TRow> = (TKind & {
|
||||
readonly row: TRow;
|
||||
})['type'];
|
||||
@@ -0,0 +1,11 @@
|
||||
import type { AddAliasToSelection } from '~/query-builders/select.types.ts';
|
||||
import type { ColumnsSelection } from '~/sql/sql.ts';
|
||||
import type { Subquery, WithSubquery } from '~/subquery.ts';
|
||||
|
||||
export type SubqueryWithSelection<TSelection extends ColumnsSelection, TAlias extends string> =
|
||||
& Subquery<TAlias, AddAliasToSelection<TSelection, TAlias, 'gel'>>
|
||||
& AddAliasToSelection<TSelection, TAlias, 'gel'>;
|
||||
|
||||
export type WithSubqueryWithSelection<TSelection extends ColumnsSelection, TAlias extends string> =
|
||||
& WithSubquery<TAlias, AddAliasToSelection<TSelection, TAlias, 'gel'>>
|
||||
& AddAliasToSelection<TSelection, TAlias, 'gel'>;
|
||||
@@ -0,0 +1,257 @@
|
||||
import type { BuildColumns, BuildExtraConfigColumns } from '~/column-builder.ts';
|
||||
import { entityKind } from '~/entity.ts';
|
||||
import { Table, type TableConfig as TableConfigBase, type UpdateTableConfig } from '~/table.ts';
|
||||
import type { CheckBuilder } from './checks.ts';
|
||||
import { type GelColumnsBuilders, getGelColumnBuilders } from './columns/all.ts';
|
||||
import type { GelColumn, GelColumnBuilder, GelColumnBuilderBase, GelExtraConfigColumn } from './columns/common.ts';
|
||||
import type { ForeignKey, ForeignKeyBuilder } from './foreign-keys.ts';
|
||||
import type { AnyIndexBuilder } from './indexes.ts';
|
||||
import type { GelPolicy } from './policies.ts';
|
||||
import type { PrimaryKeyBuilder } from './primary-keys.ts';
|
||||
import type { UniqueConstraintBuilder } from './unique-constraint.ts';
|
||||
|
||||
export type GelTableExtraConfigValue =
|
||||
| AnyIndexBuilder
|
||||
| CheckBuilder
|
||||
| ForeignKeyBuilder
|
||||
| PrimaryKeyBuilder
|
||||
| UniqueConstraintBuilder
|
||||
| GelPolicy;
|
||||
|
||||
export type GelTableExtraConfig = Record<
|
||||
string,
|
||||
GelTableExtraConfigValue
|
||||
>;
|
||||
|
||||
export type TableConfig = TableConfigBase<GelColumn>;
|
||||
|
||||
/** @internal */
|
||||
export const InlineForeignKeys = Symbol.for('drizzle:GelInlineForeignKeys');
|
||||
/** @internal */
|
||||
export const EnableRLS = Symbol.for('drizzle:EnableRLS');
|
||||
|
||||
export class GelTable<T extends TableConfig = TableConfig> extends Table<T> {
|
||||
static override readonly [entityKind]: string = 'GelTable';
|
||||
|
||||
/** @internal */
|
||||
static override readonly Symbol = Object.assign({}, Table.Symbol, {
|
||||
InlineForeignKeys: InlineForeignKeys as typeof InlineForeignKeys,
|
||||
EnableRLS: EnableRLS as typeof EnableRLS,
|
||||
});
|
||||
|
||||
/**@internal */
|
||||
[InlineForeignKeys]: ForeignKey[] = [];
|
||||
|
||||
/** @internal */
|
||||
[EnableRLS]: boolean = false;
|
||||
|
||||
/** @internal */
|
||||
override [Table.Symbol.ExtraConfigBuilder]: ((self: Record<string, GelColumn>) => GelTableExtraConfig) | undefined =
|
||||
undefined;
|
||||
|
||||
/** @internal */
|
||||
override [Table.Symbol.ExtraConfigColumns]: Record<string, GelExtraConfigColumn> = {};
|
||||
}
|
||||
|
||||
export type AnyGelTable<TPartial extends Partial<TableConfig> = {}> = GelTable<
|
||||
UpdateTableConfig<TableConfig, TPartial>
|
||||
>;
|
||||
|
||||
export type GelTableWithColumns<T extends TableConfig> =
|
||||
& GelTable<T>
|
||||
& {
|
||||
[Key in keyof T['columns']]: T['columns'][Key];
|
||||
}
|
||||
& {
|
||||
enableRLS: () => Omit<
|
||||
GelTableWithColumns<T>,
|
||||
'enableRLS'
|
||||
>;
|
||||
};
|
||||
|
||||
/** @internal */
|
||||
export function gelTableWithSchema<
|
||||
TTableName extends string,
|
||||
TSchemaName extends string | undefined,
|
||||
TColumnsMap extends Record<string, GelColumnBuilderBase>,
|
||||
>(
|
||||
name: TTableName,
|
||||
columns: TColumnsMap | ((columnTypes: GelColumnsBuilders) => TColumnsMap),
|
||||
extraConfig:
|
||||
| ((
|
||||
self: BuildExtraConfigColumns<TTableName, TColumnsMap, 'gel'>,
|
||||
) => GelTableExtraConfig | GelTableExtraConfigValue[])
|
||||
| undefined,
|
||||
schema: TSchemaName,
|
||||
baseName = name,
|
||||
): GelTableWithColumns<{
|
||||
name: TTableName;
|
||||
schema: TSchemaName;
|
||||
columns: BuildColumns<TTableName, TColumnsMap, 'gel'>;
|
||||
dialect: 'gel';
|
||||
}> {
|
||||
const rawTable = new GelTable<{
|
||||
name: TTableName;
|
||||
schema: TSchemaName;
|
||||
columns: BuildColumns<TTableName, TColumnsMap, 'gel'>;
|
||||
dialect: 'gel';
|
||||
}>(name, schema, baseName);
|
||||
|
||||
const parsedColumns: TColumnsMap = typeof columns === 'function' ? columns(getGelColumnBuilders()) : columns;
|
||||
|
||||
const builtColumns = Object.fromEntries(
|
||||
Object.entries(parsedColumns).map(([name, colBuilderBase]) => {
|
||||
const colBuilder = colBuilderBase as GelColumnBuilder;
|
||||
colBuilder.setName(name);
|
||||
const column = colBuilder.build(rawTable);
|
||||
rawTable[InlineForeignKeys].push(...colBuilder.buildForeignKeys(column, rawTable));
|
||||
return [name, column];
|
||||
}),
|
||||
) as unknown as BuildColumns<TTableName, TColumnsMap, 'gel'>;
|
||||
|
||||
const builtColumnsForExtraConfig = Object.fromEntries(
|
||||
Object.entries(parsedColumns).map(([name, colBuilderBase]) => {
|
||||
const colBuilder = colBuilderBase as GelColumnBuilder;
|
||||
colBuilder.setName(name);
|
||||
const column = colBuilder.buildExtraConfigColumn(rawTable);
|
||||
return [name, column];
|
||||
}),
|
||||
) as unknown as BuildExtraConfigColumns<TTableName, TColumnsMap, 'gel'>;
|
||||
|
||||
const table = Object.assign(rawTable, builtColumns);
|
||||
|
||||
table[Table.Symbol.Columns] = builtColumns;
|
||||
table[Table.Symbol.ExtraConfigColumns] = builtColumnsForExtraConfig;
|
||||
|
||||
if (extraConfig) {
|
||||
table[GelTable.Symbol.ExtraConfigBuilder] = extraConfig as any;
|
||||
}
|
||||
|
||||
return Object.assign(table, {
|
||||
enableRLS: () => {
|
||||
table[GelTable.Symbol.EnableRLS] = true;
|
||||
return table as GelTableWithColumns<{
|
||||
name: TTableName;
|
||||
schema: TSchemaName;
|
||||
columns: BuildColumns<TTableName, TColumnsMap, 'gel'>;
|
||||
dialect: 'gel';
|
||||
}>;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export interface GelTableFn<TSchema extends string | undefined = undefined> {
|
||||
/**
|
||||
* @deprecated The third parameter of GelTable is changing and will only accept an array instead of an object
|
||||
*
|
||||
* @example
|
||||
* Deprecated version:
|
||||
* ```ts
|
||||
* export const users = gelTable("users", {
|
||||
* id: integer(),
|
||||
* }, (t) => ({
|
||||
* idx: index('custom_name').on(t.id)
|
||||
* }));
|
||||
* ```
|
||||
*
|
||||
* New API:
|
||||
* ```ts
|
||||
* export const users = gelTable("users", {
|
||||
* id: integer(),
|
||||
* }, (t) => [
|
||||
* index('custom_name').on(t.id)
|
||||
* ]);
|
||||
* ```
|
||||
*/
|
||||
<
|
||||
TTableName extends string,
|
||||
TColumnsMap extends Record<string, GelColumnBuilderBase>,
|
||||
>(
|
||||
name: TTableName,
|
||||
columns: TColumnsMap,
|
||||
extraConfig: (
|
||||
self: BuildExtraConfigColumns<TTableName, TColumnsMap, 'gel'>,
|
||||
) => GelTableExtraConfig,
|
||||
): GelTableWithColumns<{
|
||||
name: TTableName;
|
||||
schema: TSchema;
|
||||
columns: BuildColumns<TTableName, TColumnsMap, 'gel'>;
|
||||
dialect: 'gel';
|
||||
}>;
|
||||
|
||||
/**
|
||||
* @deprecated The third parameter of gelTable is changing and will only accept an array instead of an object
|
||||
*
|
||||
* @example
|
||||
* Deprecated version:
|
||||
* ```ts
|
||||
* export const users = gelTable("users", {
|
||||
* id: integer(),
|
||||
* }, (t) => ({
|
||||
* idx: index('custom_name').on(t.id)
|
||||
* }));
|
||||
* ```
|
||||
*
|
||||
* New API:
|
||||
* ```ts
|
||||
* export const users = gelTable("users", {
|
||||
* id: integer(),
|
||||
* }, (t) => [
|
||||
* index('custom_name').on(t.id)
|
||||
* ]);
|
||||
* ```
|
||||
*/
|
||||
<
|
||||
TTableName extends string,
|
||||
TColumnsMap extends Record<string, GelColumnBuilderBase>,
|
||||
>(
|
||||
name: TTableName,
|
||||
columns: (columnTypes: GelColumnsBuilders) => TColumnsMap,
|
||||
extraConfig: (self: BuildExtraConfigColumns<TTableName, TColumnsMap, 'gel'>) => GelTableExtraConfig,
|
||||
): GelTableWithColumns<{
|
||||
name: TTableName;
|
||||
schema: TSchema;
|
||||
columns: BuildColumns<TTableName, TColumnsMap, 'gel'>;
|
||||
dialect: 'gel';
|
||||
}>;
|
||||
|
||||
<
|
||||
TTableName extends string,
|
||||
TColumnsMap extends Record<string, GelColumnBuilderBase>,
|
||||
>(
|
||||
name: TTableName,
|
||||
columns: TColumnsMap,
|
||||
extraConfig?: (
|
||||
self: BuildExtraConfigColumns<TTableName, TColumnsMap, 'gel'>,
|
||||
) => GelTableExtraConfigValue[],
|
||||
): GelTableWithColumns<{
|
||||
name: TTableName;
|
||||
schema: TSchema;
|
||||
columns: BuildColumns<TTableName, TColumnsMap, 'gel'>;
|
||||
dialect: 'gel';
|
||||
}>;
|
||||
|
||||
<
|
||||
TTableName extends string,
|
||||
TColumnsMap extends Record<string, GelColumnBuilderBase>,
|
||||
>(
|
||||
name: TTableName,
|
||||
columns: (columnTypes: GelColumnsBuilders) => TColumnsMap,
|
||||
extraConfig?: (self: BuildExtraConfigColumns<TTableName, TColumnsMap, 'gel'>) => GelTableExtraConfigValue[],
|
||||
): GelTableWithColumns<{
|
||||
name: TTableName;
|
||||
schema: TSchema;
|
||||
columns: BuildColumns<TTableName, TColumnsMap, 'gel'>;
|
||||
dialect: 'gel';
|
||||
}>;
|
||||
}
|
||||
|
||||
export const gelTable: GelTableFn = (name, columns, extraConfig) => {
|
||||
return gelTableWithSchema(name, columns, extraConfig, undefined);
|
||||
};
|
||||
|
||||
export function gelTableCreator(customizeTableName: (name: string) => string): GelTableFn {
|
||||
return (name, columns, extraConfig) => {
|
||||
return gelTableWithSchema(customizeTableName(name) as typeof name, columns, extraConfig, undefined, name);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import { entityKind } from '~/entity.ts';
|
||||
import { TableName } from '~/table.utils.ts';
|
||||
import type { GelColumn } from './columns/index.ts';
|
||||
import type { GelTable } from './table.ts';
|
||||
|
||||
export function unique(name?: string): UniqueOnConstraintBuilder {
|
||||
return new UniqueOnConstraintBuilder(name);
|
||||
}
|
||||
|
||||
export function uniqueKeyName(table: GelTable, columns: string[]) {
|
||||
return `${table[TableName]}_${columns.join('_')}_unique`;
|
||||
}
|
||||
|
||||
export class UniqueConstraintBuilder {
|
||||
static readonly [entityKind]: string = 'GelUniqueConstraintBuilder';
|
||||
|
||||
/** @internal */
|
||||
columns: GelColumn[];
|
||||
/** @internal */
|
||||
nullsNotDistinctConfig = false;
|
||||
|
||||
constructor(
|
||||
columns: GelColumn[],
|
||||
private name?: string,
|
||||
) {
|
||||
this.columns = columns;
|
||||
}
|
||||
|
||||
nullsNotDistinct() {
|
||||
this.nullsNotDistinctConfig = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
build(table: GelTable): UniqueConstraint {
|
||||
return new UniqueConstraint(table, this.columns, this.nullsNotDistinctConfig, this.name);
|
||||
}
|
||||
}
|
||||
|
||||
export class UniqueOnConstraintBuilder {
|
||||
static readonly [entityKind]: string = 'GelUniqueOnConstraintBuilder';
|
||||
|
||||
/** @internal */
|
||||
name?: string;
|
||||
|
||||
constructor(
|
||||
name?: string,
|
||||
) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
on(...columns: [GelColumn, ...GelColumn[]]) {
|
||||
return new UniqueConstraintBuilder(columns, this.name);
|
||||
}
|
||||
}
|
||||
|
||||
export class UniqueConstraint {
|
||||
static readonly [entityKind]: string = 'GelUniqueConstraint';
|
||||
|
||||
readonly columns: GelColumn[];
|
||||
readonly name?: string;
|
||||
readonly nullsNotDistinct: boolean = false;
|
||||
|
||||
constructor(readonly table: GelTable, columns: GelColumn[], nullsNotDistinct: boolean, name?: string) {
|
||||
this.columns = columns;
|
||||
this.name = name ?? uniqueKeyName(this.table, this.columns.map((column) => column.name));
|
||||
this.nullsNotDistinct = nullsNotDistinct;
|
||||
}
|
||||
|
||||
getName() {
|
||||
return this.name;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user