drizzle-databend 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +149 -0
- package/dist/client.d.ts +22 -0
- package/dist/columns.d.ts +70 -0
- package/dist/dialect.d.ts +10 -0
- package/dist/driver.d.ts +50 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.mjs +887 -0
- package/dist/migrator.d.ts +4 -0
- package/dist/pool.d.ts +21 -0
- package/dist/session.d.ts +55 -0
- package/dist/sql/result-mapper.d.ts +7 -0
- package/dist/sql/selection.d.ts +2 -0
- package/package.json +53 -0
- package/src/client.ts +140 -0
- package/src/columns.ts +164 -0
- package/src/dialect.ts +109 -0
- package/src/driver.ts +268 -0
- package/src/index.ts +6 -0
- package/src/migrator.ts +22 -0
- package/src/pool.ts +233 -0
- package/src/session.ts +311 -0
- package/src/sql/result-mapper.ts +234 -0
- package/src/sql/selection.ts +60 -0
package/src/session.ts
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import { entityKind } from 'drizzle-orm/entity';
|
|
2
|
+
import type { Logger } from 'drizzle-orm/logger';
|
|
3
|
+
import { NoopLogger } from 'drizzle-orm/logger';
|
|
4
|
+
import { PgTransaction } from 'drizzle-orm/pg-core';
|
|
5
|
+
import type { SelectedFieldsOrdered } from 'drizzle-orm/pg-core/query-builders/select.types';
|
|
6
|
+
import type {
|
|
7
|
+
PgTransactionConfig,
|
|
8
|
+
PreparedQueryConfig,
|
|
9
|
+
PgQueryResultHKT,
|
|
10
|
+
} from 'drizzle-orm/pg-core/session';
|
|
11
|
+
import { PgPreparedQuery, PgSession } from 'drizzle-orm/pg-core/session';
|
|
12
|
+
import type {
|
|
13
|
+
RelationalSchemaConfig,
|
|
14
|
+
TablesRelationalConfig,
|
|
15
|
+
} from 'drizzle-orm/relations';
|
|
16
|
+
import { fillPlaceholders, type Query, SQL, sql } from 'drizzle-orm/sql/sql';
|
|
17
|
+
import type { Assume } from 'drizzle-orm/utils';
|
|
18
|
+
import { mapResultRow } from './sql/result-mapper.ts';
|
|
19
|
+
import { TransactionRollbackError } from 'drizzle-orm/errors';
|
|
20
|
+
import type { DatabendDialect } from './dialect.ts';
|
|
21
|
+
import type {
|
|
22
|
+
DatabendClientLike,
|
|
23
|
+
DatabendConnectionPool,
|
|
24
|
+
RowData,
|
|
25
|
+
} from './client.ts';
|
|
26
|
+
import {
|
|
27
|
+
executeArraysOnClient,
|
|
28
|
+
executeOnClient,
|
|
29
|
+
prepareParams,
|
|
30
|
+
isPool,
|
|
31
|
+
} from './client.ts';
|
|
32
|
+
import type { Connection } from 'databend-driver';
|
|
33
|
+
|
|
34
|
+
export type { DatabendClientLike, RowData } from './client.ts';
|
|
35
|
+
|
|
36
|
+
export class DatabendPreparedQuery<
|
|
37
|
+
T extends PreparedQueryConfig,
|
|
38
|
+
> extends PgPreparedQuery<T> {
|
|
39
|
+
static readonly [entityKind]: string = 'DatabendPreparedQuery';
|
|
40
|
+
|
|
41
|
+
constructor(
|
|
42
|
+
private client: DatabendClientLike,
|
|
43
|
+
private queryString: string,
|
|
44
|
+
private params: unknown[],
|
|
45
|
+
private logger: Logger,
|
|
46
|
+
private fields: SelectedFieldsOrdered | undefined,
|
|
47
|
+
private _isResponseInArrayMode: boolean,
|
|
48
|
+
private customResultMapper:
|
|
49
|
+
| ((rows: unknown[][]) => T['execute'])
|
|
50
|
+
| undefined
|
|
51
|
+
) {
|
|
52
|
+
super({ sql: queryString, params });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async execute(
|
|
56
|
+
placeholderValues: Record<string, unknown> | undefined = {}
|
|
57
|
+
): Promise<T['execute']> {
|
|
58
|
+
const params = prepareParams(
|
|
59
|
+
fillPlaceholders(this.params, placeholderValues)
|
|
60
|
+
);
|
|
61
|
+
this.logger.logQuery(this.queryString, params);
|
|
62
|
+
|
|
63
|
+
const { fields, joinsNotNullableMap, customResultMapper } =
|
|
64
|
+
this as typeof this & { joinsNotNullableMap?: Record<string, boolean> };
|
|
65
|
+
|
|
66
|
+
if (fields) {
|
|
67
|
+
const { rows } = await executeArraysOnClient(
|
|
68
|
+
this.client,
|
|
69
|
+
this.queryString,
|
|
70
|
+
params
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
if (rows.length === 0) {
|
|
74
|
+
return [] as T['execute'];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return customResultMapper
|
|
78
|
+
? customResultMapper(rows)
|
|
79
|
+
: rows.map((row) =>
|
|
80
|
+
mapResultRow<T['execute']>(fields, row, joinsNotNullableMap)
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const rows = await executeOnClient(this.client, this.queryString, params);
|
|
85
|
+
|
|
86
|
+
return rows as T['execute'];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
all(
|
|
90
|
+
placeholderValues: Record<string, unknown> | undefined = {}
|
|
91
|
+
): Promise<T['all']> {
|
|
92
|
+
return this.execute(placeholderValues);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
isResponseInArrayMode(): boolean {
|
|
96
|
+
return this._isResponseInArrayMode;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface DatabendSessionOptions {
|
|
101
|
+
logger?: Logger;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export class DatabendSession<
|
|
105
|
+
TFullSchema extends Record<string, unknown> = Record<string, never>,
|
|
106
|
+
TSchema extends TablesRelationalConfig = Record<string, never>,
|
|
107
|
+
> extends PgSession<DatabendQueryResultHKT, TFullSchema, TSchema> {
|
|
108
|
+
static readonly [entityKind]: string = 'DatabendSession';
|
|
109
|
+
|
|
110
|
+
protected override dialect: DatabendDialect;
|
|
111
|
+
private logger: Logger;
|
|
112
|
+
private rollbackOnly = false;
|
|
113
|
+
|
|
114
|
+
constructor(
|
|
115
|
+
private client: DatabendClientLike,
|
|
116
|
+
dialect: DatabendDialect,
|
|
117
|
+
private schema: RelationalSchemaConfig<TSchema> | undefined,
|
|
118
|
+
private options: DatabendSessionOptions = {}
|
|
119
|
+
) {
|
|
120
|
+
super(dialect);
|
|
121
|
+
this.dialect = dialect;
|
|
122
|
+
this.logger = options.logger ?? new NoopLogger();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
prepareQuery<T extends PreparedQueryConfig = PreparedQueryConfig>(
|
|
126
|
+
query: Query,
|
|
127
|
+
fields: SelectedFieldsOrdered | undefined,
|
|
128
|
+
name: string | undefined,
|
|
129
|
+
isResponseInArrayMode: boolean,
|
|
130
|
+
customResultMapper?: (rows: unknown[][]) => T['execute']
|
|
131
|
+
): PgPreparedQuery<T> {
|
|
132
|
+
void name;
|
|
133
|
+
return new DatabendPreparedQuery(
|
|
134
|
+
this.client,
|
|
135
|
+
query.sql,
|
|
136
|
+
query.params,
|
|
137
|
+
this.logger,
|
|
138
|
+
fields,
|
|
139
|
+
isResponseInArrayMode,
|
|
140
|
+
customResultMapper
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
override async transaction<T>(
|
|
145
|
+
transaction: (tx: DatabendTransaction<TFullSchema, TSchema>) => Promise<T>,
|
|
146
|
+
config?: PgTransactionConfig
|
|
147
|
+
): Promise<T> {
|
|
148
|
+
let pinnedConnection: Connection | undefined;
|
|
149
|
+
let pool: DatabendConnectionPool | undefined;
|
|
150
|
+
|
|
151
|
+
let clientForTx: DatabendClientLike = this.client;
|
|
152
|
+
if (isPool(this.client)) {
|
|
153
|
+
pool = this.client;
|
|
154
|
+
pinnedConnection = await pool.acquire();
|
|
155
|
+
clientForTx = pinnedConnection;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const session = new DatabendSession(
|
|
159
|
+
clientForTx,
|
|
160
|
+
this.dialect,
|
|
161
|
+
this.schema,
|
|
162
|
+
this.options
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
const tx = new DatabendTransaction<TFullSchema, TSchema>(
|
|
166
|
+
this.dialect,
|
|
167
|
+
session,
|
|
168
|
+
this.schema
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
await tx.execute(sql`BEGIN`);
|
|
173
|
+
|
|
174
|
+
if (config) {
|
|
175
|
+
await tx.setTransaction(config);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
const result = await transaction(tx);
|
|
180
|
+
if (session.isRollbackOnly()) {
|
|
181
|
+
await tx.execute(sql`ROLLBACK`);
|
|
182
|
+
throw new TransactionRollbackError();
|
|
183
|
+
}
|
|
184
|
+
await tx.execute(sql`COMMIT`);
|
|
185
|
+
return result;
|
|
186
|
+
} catch (error) {
|
|
187
|
+
await tx.execute(sql`ROLLBACK`);
|
|
188
|
+
throw error;
|
|
189
|
+
}
|
|
190
|
+
} finally {
|
|
191
|
+
if (pinnedConnection && pool) {
|
|
192
|
+
await pool.release(pinnedConnection);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
markRollbackOnly(): void {
|
|
198
|
+
this.rollbackOnly = true;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
isRollbackOnly(): boolean {
|
|
202
|
+
return this.rollbackOnly;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
type DatabendTransactionInternals<
|
|
207
|
+
TFullSchema extends Record<string, unknown> = Record<string, never>,
|
|
208
|
+
TSchema extends TablesRelationalConfig = Record<string, never>,
|
|
209
|
+
> = {
|
|
210
|
+
dialect: DatabendDialect;
|
|
211
|
+
session: DatabendSession<TFullSchema, TSchema>;
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
type DatabendTransactionWithInternals<
|
|
215
|
+
TFullSchema extends Record<string, unknown> = Record<string, never>,
|
|
216
|
+
TSchema extends TablesRelationalConfig = Record<string, never>,
|
|
217
|
+
> = DatabendTransactionInternals<TFullSchema, TSchema> &
|
|
218
|
+
DatabendTransaction<TFullSchema, TSchema>;
|
|
219
|
+
|
|
220
|
+
const VALID_TRANSACTION_ISOLATION_LEVELS = new Set<string>([
|
|
221
|
+
'read uncommitted',
|
|
222
|
+
'read committed',
|
|
223
|
+
'repeatable read',
|
|
224
|
+
'serializable',
|
|
225
|
+
]);
|
|
226
|
+
|
|
227
|
+
const VALID_TRANSACTION_ACCESS_MODES = new Set<string>([
|
|
228
|
+
'read only',
|
|
229
|
+
'read write',
|
|
230
|
+
]);
|
|
231
|
+
|
|
232
|
+
export class DatabendTransaction<
|
|
233
|
+
TFullSchema extends Record<string, unknown>,
|
|
234
|
+
TSchema extends TablesRelationalConfig,
|
|
235
|
+
> extends PgTransaction<DatabendQueryResultHKT, TFullSchema, TSchema> {
|
|
236
|
+
static readonly [entityKind]: string = 'DatabendTransaction';
|
|
237
|
+
|
|
238
|
+
rollback(): never {
|
|
239
|
+
throw new TransactionRollbackError();
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
getTransactionConfigSQL(config: PgTransactionConfig): SQL {
|
|
243
|
+
if (
|
|
244
|
+
config.isolationLevel &&
|
|
245
|
+
!VALID_TRANSACTION_ISOLATION_LEVELS.has(config.isolationLevel)
|
|
246
|
+
) {
|
|
247
|
+
throw new Error(
|
|
248
|
+
`Invalid transaction isolation level "${config.isolationLevel}". Expected one of: ${Array.from(
|
|
249
|
+
VALID_TRANSACTION_ISOLATION_LEVELS
|
|
250
|
+
).join(', ')}.`
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (
|
|
255
|
+
config.accessMode &&
|
|
256
|
+
!VALID_TRANSACTION_ACCESS_MODES.has(config.accessMode)
|
|
257
|
+
) {
|
|
258
|
+
throw new Error(
|
|
259
|
+
`Invalid transaction access mode "${config.accessMode}". Expected one of: ${Array.from(
|
|
260
|
+
VALID_TRANSACTION_ACCESS_MODES
|
|
261
|
+
).join(', ')}.`
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const chunks: string[] = [];
|
|
266
|
+
if (config.isolationLevel) {
|
|
267
|
+
chunks.push(`isolation level ${config.isolationLevel}`);
|
|
268
|
+
}
|
|
269
|
+
if (config.accessMode) {
|
|
270
|
+
chunks.push(config.accessMode);
|
|
271
|
+
}
|
|
272
|
+
return sql.raw(chunks.join(' '));
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
setTransaction(config: PgTransactionConfig): Promise<void> {
|
|
276
|
+
type Tx = DatabendTransactionWithInternals<TFullSchema, TSchema>;
|
|
277
|
+
return (this as unknown as Tx).session.execute(
|
|
278
|
+
sql`SET TRANSACTION ${this.getTransactionConfigSQL(config)}`
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
override async transaction<T>(
|
|
283
|
+
transaction: (tx: DatabendTransaction<TFullSchema, TSchema>) => Promise<T>
|
|
284
|
+
): Promise<T> {
|
|
285
|
+
// Databend does not support savepoints. Use rollback-only fallback.
|
|
286
|
+
type Tx = DatabendTransactionWithInternals<TFullSchema, TSchema>;
|
|
287
|
+
const internals = this as unknown as Tx;
|
|
288
|
+
|
|
289
|
+
const nestedTx = new DatabendTransaction<TFullSchema, TSchema>(
|
|
290
|
+
internals.dialect,
|
|
291
|
+
internals.session,
|
|
292
|
+
this.schema,
|
|
293
|
+
this.nestedIndex + 1
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
return transaction(nestedTx).catch((error) => {
|
|
297
|
+
(
|
|
298
|
+
internals.session as DatabendSession<TFullSchema, TSchema>
|
|
299
|
+
).markRollbackOnly();
|
|
300
|
+
throw error;
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export type GenericRowData<T extends RowData = RowData> = T;
|
|
306
|
+
|
|
307
|
+
export type GenericTableData<T = RowData> = T[];
|
|
308
|
+
|
|
309
|
+
export interface DatabendQueryResultHKT extends PgQueryResultHKT {
|
|
310
|
+
type: GenericTableData<Assume<this['row'], RowData>>;
|
|
311
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Column,
|
|
3
|
+
SQL,
|
|
4
|
+
getTableName,
|
|
5
|
+
is,
|
|
6
|
+
type AnyColumn,
|
|
7
|
+
type DriverValueDecoder,
|
|
8
|
+
type SelectedFieldsOrdered,
|
|
9
|
+
} from 'drizzle-orm';
|
|
10
|
+
import {
|
|
11
|
+
PgCustomColumn,
|
|
12
|
+
PgDate,
|
|
13
|
+
PgDateString,
|
|
14
|
+
PgTime,
|
|
15
|
+
PgTimestamp,
|
|
16
|
+
PgTimestampString,
|
|
17
|
+
} from 'drizzle-orm/pg-core';
|
|
18
|
+
|
|
19
|
+
type SQLInternal<T = unknown> = SQL<T> & {
|
|
20
|
+
decoder: DriverValueDecoder<T, any>;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type DecoderInput<TDecoder extends DriverValueDecoder<unknown, unknown>> =
|
|
24
|
+
Parameters<TDecoder['mapFromDriverValue']>[0];
|
|
25
|
+
|
|
26
|
+
function toDecoderInput<TDecoder extends DriverValueDecoder<unknown, unknown>>(
|
|
27
|
+
decoder: TDecoder,
|
|
28
|
+
value: unknown
|
|
29
|
+
): DecoderInput<TDecoder> {
|
|
30
|
+
void decoder;
|
|
31
|
+
return value as DecoderInput<TDecoder>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function normalizeTimestampString(
|
|
35
|
+
value: unknown,
|
|
36
|
+
withTimezone: boolean
|
|
37
|
+
): string | unknown {
|
|
38
|
+
if (value instanceof Date) {
|
|
39
|
+
const iso = value.toISOString().replace('T', ' ');
|
|
40
|
+
return withTimezone ? iso.replace('Z', '+00') : iso.replace('Z', '');
|
|
41
|
+
}
|
|
42
|
+
if (typeof value === 'string') {
|
|
43
|
+
const normalized = value.replace('T', ' ');
|
|
44
|
+
if (withTimezone) {
|
|
45
|
+
return normalized.includes('+') ? normalized : `${normalized}+00`;
|
|
46
|
+
}
|
|
47
|
+
return normalized.replace(/\+00$/, '');
|
|
48
|
+
}
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function normalizeTimestamp(
|
|
53
|
+
value: unknown,
|
|
54
|
+
withTimezone: boolean
|
|
55
|
+
): Date | unknown {
|
|
56
|
+
if (value instanceof Date) {
|
|
57
|
+
return value;
|
|
58
|
+
}
|
|
59
|
+
if (typeof value === 'string') {
|
|
60
|
+
const hasOffset =
|
|
61
|
+
value.endsWith('Z') || /[+-]\d{2}:?\d{2}$/.test(value.trim());
|
|
62
|
+
const spaced = value.replace(' ', 'T');
|
|
63
|
+
const normalized = withTimezone || hasOffset ? spaced : `${spaced}+00`;
|
|
64
|
+
return new Date(normalized);
|
|
65
|
+
}
|
|
66
|
+
return value;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function normalizeDateString(value: unknown): string | unknown {
|
|
70
|
+
if (value instanceof Date) {
|
|
71
|
+
return value.toISOString().slice(0, 10);
|
|
72
|
+
}
|
|
73
|
+
if (typeof value === 'string') {
|
|
74
|
+
return value.slice(0, 10);
|
|
75
|
+
}
|
|
76
|
+
return value;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function normalizeDateValue(value: unknown): Date | unknown {
|
|
80
|
+
if (value instanceof Date) {
|
|
81
|
+
return value;
|
|
82
|
+
}
|
|
83
|
+
if (typeof value === 'string') {
|
|
84
|
+
return new Date(`${value.slice(0, 10)}T00:00:00Z`);
|
|
85
|
+
}
|
|
86
|
+
return value;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function normalizeTime(value: unknown): string | unknown {
|
|
90
|
+
if (typeof value === 'bigint') {
|
|
91
|
+
const totalMillis = Number(value) / 1000;
|
|
92
|
+
const date = new Date(totalMillis);
|
|
93
|
+
return date.toISOString().split('T')[1]!.replace('Z', '');
|
|
94
|
+
}
|
|
95
|
+
if (value instanceof Date) {
|
|
96
|
+
return value.toISOString().split('T')[1]!.replace('Z', '');
|
|
97
|
+
}
|
|
98
|
+
return value;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function mapDriverValue(
|
|
102
|
+
decoder: DriverValueDecoder<unknown, unknown>,
|
|
103
|
+
rawValue: unknown
|
|
104
|
+
): unknown {
|
|
105
|
+
if (is(decoder, PgTimestampString)) {
|
|
106
|
+
return decoder.mapFromDriverValue(
|
|
107
|
+
toDecoderInput(
|
|
108
|
+
decoder,
|
|
109
|
+
normalizeTimestampString(rawValue, decoder.withTimezone)
|
|
110
|
+
)
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (is(decoder, PgTimestamp)) {
|
|
115
|
+
const normalized = normalizeTimestamp(rawValue, decoder.withTimezone);
|
|
116
|
+
if (normalized instanceof Date) {
|
|
117
|
+
return normalized;
|
|
118
|
+
}
|
|
119
|
+
return decoder.mapFromDriverValue(toDecoderInput(decoder, normalized));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (is(decoder, PgDateString)) {
|
|
123
|
+
return decoder.mapFromDriverValue(
|
|
124
|
+
toDecoderInput(decoder, normalizeDateString(rawValue))
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (is(decoder, PgDate)) {
|
|
129
|
+
return decoder.mapFromDriverValue(
|
|
130
|
+
toDecoderInput(decoder, normalizeDateValue(rawValue))
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (is(decoder, PgTime)) {
|
|
135
|
+
return decoder.mapFromDriverValue(
|
|
136
|
+
toDecoderInput(decoder, normalizeTime(rawValue))
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return decoder.mapFromDriverValue(toDecoderInput(decoder, rawValue));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function mapResultRow<TResult>(
|
|
144
|
+
columns: SelectedFieldsOrdered<AnyColumn>,
|
|
145
|
+
row: unknown[],
|
|
146
|
+
joinsNotNullableMap: Record<string, boolean> | undefined
|
|
147
|
+
): TResult {
|
|
148
|
+
const nullifyMap: Record<string, string | false> = {};
|
|
149
|
+
|
|
150
|
+
const result = columns.reduce<Record<string, any>>(
|
|
151
|
+
(acc, { path, field }, columnIndex) => {
|
|
152
|
+
let decoder: DriverValueDecoder<unknown, unknown>;
|
|
153
|
+
if (is(field, Column)) {
|
|
154
|
+
decoder = field;
|
|
155
|
+
} else if (is(field, SQL)) {
|
|
156
|
+
decoder = (field as SQLInternal).decoder;
|
|
157
|
+
} else {
|
|
158
|
+
const col = field.sql.queryChunks.find((chunk) => is(chunk, Column));
|
|
159
|
+
|
|
160
|
+
if (is(col, PgCustomColumn)) {
|
|
161
|
+
decoder = col;
|
|
162
|
+
} else {
|
|
163
|
+
decoder = (field.sql as SQLInternal).decoder;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
let node = acc;
|
|
167
|
+
for (const [pathChunkIndex, pathChunk] of path.entries()) {
|
|
168
|
+
if (pathChunkIndex < path.length - 1) {
|
|
169
|
+
if (!(pathChunk in node)) {
|
|
170
|
+
node[pathChunk] = {};
|
|
171
|
+
}
|
|
172
|
+
node = node[pathChunk];
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const rawValue = row[columnIndex]!;
|
|
177
|
+
|
|
178
|
+
const value = (node[pathChunk] =
|
|
179
|
+
rawValue === null ? null : mapDriverValue(decoder, rawValue));
|
|
180
|
+
|
|
181
|
+
if (joinsNotNullableMap && is(field, Column) && path.length === 2) {
|
|
182
|
+
const objectName = path[0]!;
|
|
183
|
+
if (!(objectName in nullifyMap)) {
|
|
184
|
+
nullifyMap[objectName] =
|
|
185
|
+
value === null ? getTableName(field.table) : false;
|
|
186
|
+
} else if (
|
|
187
|
+
typeof nullifyMap[objectName] === 'string' &&
|
|
188
|
+
nullifyMap[objectName] !== getTableName(field.table)
|
|
189
|
+
) {
|
|
190
|
+
nullifyMap[objectName] = false;
|
|
191
|
+
}
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (
|
|
196
|
+
joinsNotNullableMap &&
|
|
197
|
+
is(field, SQL.Aliased) &&
|
|
198
|
+
path.length === 2
|
|
199
|
+
) {
|
|
200
|
+
const col = field.sql.queryChunks.find((chunk) => is(chunk, Column));
|
|
201
|
+
const tableName = col?.table && getTableName(col?.table);
|
|
202
|
+
|
|
203
|
+
if (!tableName) {
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const objectName = path[0]!;
|
|
208
|
+
|
|
209
|
+
if (!(objectName in nullifyMap)) {
|
|
210
|
+
nullifyMap[objectName] = value === null ? tableName : false;
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (nullifyMap[objectName] && nullifyMap[objectName] !== tableName) {
|
|
215
|
+
nullifyMap[objectName] = false;
|
|
216
|
+
}
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return acc;
|
|
221
|
+
},
|
|
222
|
+
{}
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
if (joinsNotNullableMap && Object.keys(nullifyMap).length > 0) {
|
|
226
|
+
for (const [objectName, tableName] of Object.entries(nullifyMap)) {
|
|
227
|
+
if (typeof tableName === 'string' && !joinsNotNullableMap[tableName]) {
|
|
228
|
+
result[objectName] = null;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return result as TResult;
|
|
234
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Column, SQL, getTableName, is, sql } from 'drizzle-orm';
|
|
2
|
+
import type { SelectedFields } from 'drizzle-orm/pg-core';
|
|
3
|
+
|
|
4
|
+
function mapEntries(
|
|
5
|
+
obj: Record<string, unknown>,
|
|
6
|
+
prefix?: string,
|
|
7
|
+
fullJoin = false
|
|
8
|
+
): Record<string, unknown> {
|
|
9
|
+
return Object.fromEntries(
|
|
10
|
+
Object.entries(obj)
|
|
11
|
+
.filter(([key]) => key !== 'enableRLS')
|
|
12
|
+
.map(([key, value]) => {
|
|
13
|
+
const qualified = prefix ? `${prefix}.${key}` : key;
|
|
14
|
+
|
|
15
|
+
if (fullJoin && is(value, Column)) {
|
|
16
|
+
return [
|
|
17
|
+
key,
|
|
18
|
+
sql`${value}`
|
|
19
|
+
.mapWith(value)
|
|
20
|
+
.as(`${getTableName(value.table)}.${value.name}`),
|
|
21
|
+
];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (fullJoin && is(value, SQL)) {
|
|
25
|
+
const col = value
|
|
26
|
+
.getSQL()
|
|
27
|
+
.queryChunks.find((chunk) => is(chunk, Column));
|
|
28
|
+
|
|
29
|
+
const tableName = col?.table && getTableName(col?.table);
|
|
30
|
+
|
|
31
|
+
return [key, value.as(tableName ? `${tableName}.${key}` : key)];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (is(value, SQL) || is(value, Column)) {
|
|
35
|
+
const aliased = is(value, SQL) ? value : sql`${value}`.mapWith(value);
|
|
36
|
+
return [key, aliased.as(qualified)];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (is(value, SQL.Aliased)) {
|
|
40
|
+
return [key, value];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (typeof value === 'object' && value !== null) {
|
|
44
|
+
return [
|
|
45
|
+
key,
|
|
46
|
+
mapEntries(value as Record<string, unknown>, qualified, fullJoin),
|
|
47
|
+
];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return [key, value];
|
|
51
|
+
})
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function aliasFields(
|
|
56
|
+
fields: SelectedFields,
|
|
57
|
+
fullJoin = false
|
|
58
|
+
): SelectedFields {
|
|
59
|
+
return mapEntries(fields, undefined, fullJoin) as SelectedFields;
|
|
60
|
+
}
|