@leonardovida-md/drizzle-neo-duckdb 1.0.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 +201 -0
- package/README.md +354 -0
- package/dist/bin/duckdb-introspect.d.ts +2 -0
- package/dist/client.d.ts +10 -0
- package/dist/columns.d.ts +129 -0
- package/dist/dialect.d.ts +11 -0
- package/dist/driver.d.ts +37 -0
- package/dist/duckdb-introspect.mjs +1364 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.mjs +1564 -0
- package/dist/introspect.d.ts +53 -0
- package/dist/migrator.d.ts +4 -0
- package/dist/select-builder.d.ts +31 -0
- package/dist/session.d.ts +62 -0
- package/dist/sql/query-rewriters.d.ts +2 -0
- package/dist/sql/result-mapper.d.ts +2 -0
- package/dist/sql/selection.d.ts +2 -0
- package/dist/utils.d.ts +3 -0
- package/package.json +73 -0
- package/src/bin/duckdb-introspect.ts +117 -0
- package/src/client.ts +110 -0
- package/src/columns.ts +429 -0
- package/src/dialect.ts +136 -0
- package/src/driver.ts +131 -0
- package/src/index.ts +5 -0
- package/src/introspect.ts +853 -0
- package/src/migrator.ts +25 -0
- package/src/select-builder.ts +114 -0
- package/src/session.ts +274 -0
- package/src/sql/query-rewriters.ts +147 -0
- package/src/sql/result-mapper.ts +303 -0
- package/src/sql/selection.ts +67 -0
- package/src/utils.ts +3 -0
package/src/migrator.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { MigrationConfig } from 'drizzle-orm/migrator';
|
|
2
|
+
import { readMigrationFiles } from 'drizzle-orm/migrator';
|
|
3
|
+
import type { DuckDBDatabase } from './driver.ts';
|
|
4
|
+
import type { PgSession } from 'drizzle-orm/pg-core/session';
|
|
5
|
+
|
|
6
|
+
export type DuckDbMigrationConfig = MigrationConfig | string;
|
|
7
|
+
|
|
8
|
+
export async function migrate<TSchema extends Record<string, unknown>>(
|
|
9
|
+
db: DuckDBDatabase<TSchema>,
|
|
10
|
+
config: DuckDbMigrationConfig
|
|
11
|
+
) {
|
|
12
|
+
const migrationConfig: MigrationConfig =
|
|
13
|
+
typeof config === 'string'
|
|
14
|
+
? { migrationsFolder: config }
|
|
15
|
+
: config;
|
|
16
|
+
|
|
17
|
+
const migrations = readMigrationFiles(migrationConfig);
|
|
18
|
+
|
|
19
|
+
await db.dialect.migrate(
|
|
20
|
+
migrations,
|
|
21
|
+
// Need to work around omitted internal types from drizzle...
|
|
22
|
+
db.session as unknown as PgSession,
|
|
23
|
+
migrationConfig
|
|
24
|
+
);
|
|
25
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { is } from 'drizzle-orm/entity';
|
|
2
|
+
import {
|
|
3
|
+
PgSelectBase,
|
|
4
|
+
PgSelectBuilder,
|
|
5
|
+
type CreatePgSelectFromBuilderMode,
|
|
6
|
+
type SelectedFields,
|
|
7
|
+
type TableLikeHasEmptySelection,
|
|
8
|
+
} from 'drizzle-orm/pg-core/query-builders';
|
|
9
|
+
import {
|
|
10
|
+
PgColumn,
|
|
11
|
+
PgTable,
|
|
12
|
+
type PgSession,
|
|
13
|
+
} from 'drizzle-orm/pg-core';
|
|
14
|
+
import { Subquery, ViewBaseConfig, type SQLWrapper } from 'drizzle-orm';
|
|
15
|
+
import { PgViewBase } from 'drizzle-orm/pg-core/view-base';
|
|
16
|
+
import type {
|
|
17
|
+
GetSelectTableName,
|
|
18
|
+
GetSelectTableSelection,
|
|
19
|
+
} from 'drizzle-orm/query-builders/select.types';
|
|
20
|
+
import { SQL, type ColumnsSelection } from 'drizzle-orm/sql/sql';
|
|
21
|
+
import { aliasFields } from './sql/selection.ts';
|
|
22
|
+
import type { DuckDBDialect } from './dialect.ts';
|
|
23
|
+
import { getTableColumns, type DrizzleTypeError } from 'drizzle-orm/utils';
|
|
24
|
+
|
|
25
|
+
interface PgViewBaseInternal<
|
|
26
|
+
TName extends string = string,
|
|
27
|
+
TExisting extends boolean = boolean,
|
|
28
|
+
TSelectedFields extends ColumnsSelection = ColumnsSelection
|
|
29
|
+
> extends PgViewBase<TName, TExisting, TSelectedFields> {
|
|
30
|
+
[ViewBaseConfig]?: {
|
|
31
|
+
selectedFields: SelectedFields;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export class DuckDBSelectBuilder<
|
|
36
|
+
TSelection extends SelectedFields | undefined,
|
|
37
|
+
TBuilderMode extends 'db' | 'qb' = 'db'
|
|
38
|
+
> extends PgSelectBuilder<TSelection, TBuilderMode> {
|
|
39
|
+
private _fields: TSelection;
|
|
40
|
+
private _session: PgSession | undefined;
|
|
41
|
+
private _dialect: DuckDBDialect;
|
|
42
|
+
private _withList: Subquery[] = [];
|
|
43
|
+
private _distinct:
|
|
44
|
+
| boolean
|
|
45
|
+
| {
|
|
46
|
+
on: (PgColumn | SQLWrapper)[];
|
|
47
|
+
}
|
|
48
|
+
| undefined;
|
|
49
|
+
|
|
50
|
+
constructor(config: {
|
|
51
|
+
fields: TSelection;
|
|
52
|
+
session: PgSession | undefined;
|
|
53
|
+
dialect: DuckDBDialect;
|
|
54
|
+
withList?: Subquery[];
|
|
55
|
+
distinct?:
|
|
56
|
+
| boolean
|
|
57
|
+
| {
|
|
58
|
+
on: (PgColumn | SQLWrapper)[];
|
|
59
|
+
};
|
|
60
|
+
}) {
|
|
61
|
+
super(config);
|
|
62
|
+
this._fields = config.fields;
|
|
63
|
+
this._session = config.session;
|
|
64
|
+
this._dialect = config.dialect;
|
|
65
|
+
if (config.withList) {
|
|
66
|
+
this._withList = config.withList;
|
|
67
|
+
}
|
|
68
|
+
this._distinct = config.distinct;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
from<TFrom extends PgTable | Subquery | PgViewBaseInternal | SQL>(
|
|
72
|
+
source: TableLikeHasEmptySelection<TFrom> extends true
|
|
73
|
+
? DrizzleTypeError<"Cannot reference a data-modifying statement subquery if it doesn't contain a `returning` clause">
|
|
74
|
+
: TFrom
|
|
75
|
+
): CreatePgSelectFromBuilderMode<
|
|
76
|
+
TBuilderMode,
|
|
77
|
+
GetSelectTableName<TFrom>,
|
|
78
|
+
TSelection extends undefined ? GetSelectTableSelection<TFrom> : TSelection,
|
|
79
|
+
TSelection extends undefined ? 'single' : 'partial'
|
|
80
|
+
> {
|
|
81
|
+
const isPartialSelect = !!this._fields;
|
|
82
|
+
const src = source as TFrom;
|
|
83
|
+
|
|
84
|
+
let fields: SelectedFields;
|
|
85
|
+
if (this._fields) {
|
|
86
|
+
fields = this._fields;
|
|
87
|
+
} else if (is(src, Subquery)) {
|
|
88
|
+
fields = Object.fromEntries(
|
|
89
|
+
Object.keys(src._.selectedFields).map((key) => [
|
|
90
|
+
key,
|
|
91
|
+
src[
|
|
92
|
+
key as unknown as keyof typeof src
|
|
93
|
+
] as unknown as SelectedFields[string],
|
|
94
|
+
])
|
|
95
|
+
);
|
|
96
|
+
} else if (is(src, PgViewBase)) {
|
|
97
|
+
fields = src[ViewBaseConfig]?.selectedFields as SelectedFields;
|
|
98
|
+
} else if (is(src, SQL)) {
|
|
99
|
+
fields = {};
|
|
100
|
+
} else {
|
|
101
|
+
fields = aliasFields(getTableColumns<PgTable>(src), !isPartialSelect);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return new PgSelectBase({
|
|
105
|
+
table: src,
|
|
106
|
+
fields,
|
|
107
|
+
isPartialSelect,
|
|
108
|
+
session: this._session,
|
|
109
|
+
dialect: this._dialect,
|
|
110
|
+
withList: this._withList,
|
|
111
|
+
distinct: this._distinct,
|
|
112
|
+
}) as any;
|
|
113
|
+
}
|
|
114
|
+
}
|
package/src/session.ts
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
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 { adaptArrayOperators } from './sql/query-rewriters.ts';
|
|
19
|
+
import { mapResultRow } from './sql/result-mapper.ts';
|
|
20
|
+
import type { DuckDBDialect } from './dialect.ts';
|
|
21
|
+
import { TransactionRollbackError } from 'drizzle-orm/errors';
|
|
22
|
+
import type { DuckDBClientLike, RowData } from './client.ts';
|
|
23
|
+
import { executeOnClient, prepareParams } from './client.ts';
|
|
24
|
+
|
|
25
|
+
export type { DuckDBClientLike, RowData } from './client.ts';
|
|
26
|
+
|
|
27
|
+
export class DuckDBPreparedQuery<
|
|
28
|
+
T extends PreparedQueryConfig
|
|
29
|
+
> extends PgPreparedQuery<T> {
|
|
30
|
+
static readonly [entityKind]: string = 'DuckDBPreparedQuery';
|
|
31
|
+
|
|
32
|
+
constructor(
|
|
33
|
+
private client: DuckDBClientLike,
|
|
34
|
+
private dialect: DuckDBDialect,
|
|
35
|
+
private queryString: string,
|
|
36
|
+
private params: unknown[],
|
|
37
|
+
private logger: Logger,
|
|
38
|
+
private fields: SelectedFieldsOrdered | undefined,
|
|
39
|
+
private _isResponseInArrayMode: boolean,
|
|
40
|
+
private customResultMapper: ((rows: unknown[][]) => T['execute']) | undefined,
|
|
41
|
+
private rewriteArrays: boolean,
|
|
42
|
+
private rejectStringArrayLiterals: boolean,
|
|
43
|
+
private warnOnStringArrayLiteral?: (sql: string) => void
|
|
44
|
+
) {
|
|
45
|
+
super({ sql: queryString, params });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async execute(
|
|
49
|
+
placeholderValues: Record<string, unknown> | undefined = {}
|
|
50
|
+
): Promise<T['execute']> {
|
|
51
|
+
this.dialect.assertNoPgJsonColumns();
|
|
52
|
+
const params = prepareParams(
|
|
53
|
+
fillPlaceholders(this.params, placeholderValues),
|
|
54
|
+
{
|
|
55
|
+
rejectStringArrayLiterals: this.rejectStringArrayLiterals,
|
|
56
|
+
warnOnStringArrayLiteral: this.warnOnStringArrayLiteral
|
|
57
|
+
? () => this.warnOnStringArrayLiteral?.(this.queryString)
|
|
58
|
+
: undefined,
|
|
59
|
+
}
|
|
60
|
+
);
|
|
61
|
+
const rewrittenQuery = this.rewriteArrays
|
|
62
|
+
? adaptArrayOperators(this.queryString)
|
|
63
|
+
: this.queryString;
|
|
64
|
+
|
|
65
|
+
if (this.rewriteArrays && rewrittenQuery !== this.queryString) {
|
|
66
|
+
this.logger.logQuery(
|
|
67
|
+
`[duckdb] original query before array rewrite: ${this.queryString}`,
|
|
68
|
+
params
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
this.logger.logQuery(rewrittenQuery, params);
|
|
73
|
+
|
|
74
|
+
const {
|
|
75
|
+
fields,
|
|
76
|
+
joinsNotNullableMap,
|
|
77
|
+
customResultMapper,
|
|
78
|
+
} = this as typeof this & { joinsNotNullableMap?: Record<string, boolean> };
|
|
79
|
+
|
|
80
|
+
const rows = await executeOnClient(
|
|
81
|
+
this.client,
|
|
82
|
+
rewrittenQuery,
|
|
83
|
+
params
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
if (rows.length === 0 || !fields) {
|
|
87
|
+
return rows as T['execute'];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const rowValues = rows.map((row) => Object.values(row));
|
|
91
|
+
|
|
92
|
+
return customResultMapper
|
|
93
|
+
? customResultMapper(rowValues)
|
|
94
|
+
: rowValues.map((row) =>
|
|
95
|
+
mapResultRow<T['execute']>(fields, row, joinsNotNullableMap)
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
all(
|
|
100
|
+
placeholderValues: Record<string, unknown> | undefined = {}
|
|
101
|
+
): Promise<T['all']> {
|
|
102
|
+
return this.execute(placeholderValues);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
isResponseInArrayMode(): boolean {
|
|
106
|
+
return this._isResponseInArrayMode;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface DuckDBSessionOptions {
|
|
111
|
+
logger?: Logger;
|
|
112
|
+
rewriteArrays?: boolean;
|
|
113
|
+
rejectStringArrayLiterals?: boolean;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export class DuckDBSession<
|
|
117
|
+
TFullSchema extends Record<string, unknown> = Record<string, never>,
|
|
118
|
+
TSchema extends TablesRelationalConfig = Record<string, never>
|
|
119
|
+
> extends PgSession<DuckDBQueryResultHKT, TFullSchema, TSchema> {
|
|
120
|
+
static readonly [entityKind]: string = 'DuckDBSession';
|
|
121
|
+
|
|
122
|
+
protected override dialect: DuckDBDialect;
|
|
123
|
+
private logger: Logger;
|
|
124
|
+
private rewriteArrays: boolean;
|
|
125
|
+
private rejectStringArrayLiterals: boolean;
|
|
126
|
+
private hasWarnedArrayLiteral = false;
|
|
127
|
+
|
|
128
|
+
constructor(
|
|
129
|
+
private client: DuckDBClientLike,
|
|
130
|
+
dialect: DuckDBDialect,
|
|
131
|
+
private schema: RelationalSchemaConfig<TSchema> | undefined,
|
|
132
|
+
private options: DuckDBSessionOptions = {}
|
|
133
|
+
) {
|
|
134
|
+
super(dialect);
|
|
135
|
+
this.dialect = dialect;
|
|
136
|
+
this.logger = options.logger ?? new NoopLogger();
|
|
137
|
+
this.rewriteArrays = options.rewriteArrays ?? true;
|
|
138
|
+
this.rejectStringArrayLiterals = options.rejectStringArrayLiterals ?? false;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
prepareQuery<T extends PreparedQueryConfig = PreparedQueryConfig>(
|
|
142
|
+
query: Query,
|
|
143
|
+
fields: SelectedFieldsOrdered | undefined,
|
|
144
|
+
name: string | undefined,
|
|
145
|
+
isResponseInArrayMode: boolean,
|
|
146
|
+
customResultMapper?: (rows: unknown[][]) => T['execute']
|
|
147
|
+
): PgPreparedQuery<T> {
|
|
148
|
+
void name; // DuckDB doesn't support prepared statement names but the signature must match.
|
|
149
|
+
return new DuckDBPreparedQuery(
|
|
150
|
+
this.client,
|
|
151
|
+
this.dialect,
|
|
152
|
+
query.sql,
|
|
153
|
+
query.params,
|
|
154
|
+
this.logger,
|
|
155
|
+
fields,
|
|
156
|
+
isResponseInArrayMode,
|
|
157
|
+
customResultMapper,
|
|
158
|
+
this.rewriteArrays,
|
|
159
|
+
this.rejectStringArrayLiterals,
|
|
160
|
+
this.rejectStringArrayLiterals ? undefined : this.warnOnStringArrayLiteral
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
override async transaction<T>(
|
|
165
|
+
transaction: (tx: DuckDBTransaction<TFullSchema, TSchema>) => Promise<T>
|
|
166
|
+
): Promise<T> {
|
|
167
|
+
const session = new DuckDBSession(
|
|
168
|
+
this.client,
|
|
169
|
+
this.dialect,
|
|
170
|
+
this.schema,
|
|
171
|
+
this.options
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
const tx = new DuckDBTransaction<TFullSchema, TSchema>(
|
|
175
|
+
this.dialect,
|
|
176
|
+
session,
|
|
177
|
+
this.schema
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
await tx.execute(sql`BEGIN TRANSACTION;`);
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
const result = await transaction(tx);
|
|
184
|
+
await tx.execute(sql`commit`);
|
|
185
|
+
return result;
|
|
186
|
+
} catch (error) {
|
|
187
|
+
await tx.execute(sql`rollback`);
|
|
188
|
+
throw error;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private warnOnStringArrayLiteral = (query: string) => {
|
|
193
|
+
if (this.hasWarnedArrayLiteral) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
this.hasWarnedArrayLiteral = true;
|
|
197
|
+
this.logger.logQuery(
|
|
198
|
+
`[duckdb] ${arrayLiteralWarning}\nquery: ${query}`,
|
|
199
|
+
[]
|
|
200
|
+
);
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
type PgTransactionInternals<
|
|
205
|
+
TFullSchema extends Record<string, unknown> = Record<string, never>,
|
|
206
|
+
TSchema extends TablesRelationalConfig = Record<string, never>
|
|
207
|
+
> = {
|
|
208
|
+
dialect: DuckDBDialect;
|
|
209
|
+
session: DuckDBSession<TFullSchema, TSchema>;
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
type DuckDBTransactionWithInternals<
|
|
213
|
+
TFullSchema extends Record<string, unknown> = Record<string, never>,
|
|
214
|
+
TSchema extends TablesRelationalConfig = Record<string, never>
|
|
215
|
+
> = PgTransactionInternals<TFullSchema, TSchema> &
|
|
216
|
+
DuckDBTransaction<TFullSchema, TSchema>;
|
|
217
|
+
|
|
218
|
+
export class DuckDBTransaction<
|
|
219
|
+
TFullSchema extends Record<string, unknown>,
|
|
220
|
+
TSchema extends TablesRelationalConfig
|
|
221
|
+
> extends PgTransaction<DuckDBQueryResultHKT, TFullSchema, TSchema> {
|
|
222
|
+
static readonly [entityKind]: string = 'DuckDBTransaction';
|
|
223
|
+
|
|
224
|
+
rollback(): never {
|
|
225
|
+
throw new TransactionRollbackError();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
getTransactionConfigSQL(config: PgTransactionConfig): SQL {
|
|
229
|
+
const chunks: string[] = [];
|
|
230
|
+
if (config.isolationLevel) {
|
|
231
|
+
chunks.push(`isolation level ${config.isolationLevel}`);
|
|
232
|
+
}
|
|
233
|
+
if (config.accessMode) {
|
|
234
|
+
chunks.push(config.accessMode);
|
|
235
|
+
}
|
|
236
|
+
if (typeof config.deferrable === 'boolean') {
|
|
237
|
+
chunks.push(config.deferrable ? 'deferrable' : 'not deferrable');
|
|
238
|
+
}
|
|
239
|
+
return sql.raw(chunks.join(' '));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
setTransaction(config: PgTransactionConfig): Promise<void> {
|
|
243
|
+
type Tx = DuckDBTransactionWithInternals<TFullSchema, TSchema>;
|
|
244
|
+
return (this as unknown as Tx).session.execute(
|
|
245
|
+
sql`set transaction ${this.getTransactionConfigSQL(config)}`
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
override async transaction<T>(
|
|
250
|
+
transaction: (tx: DuckDBTransaction<TFullSchema, TSchema>) => Promise<T>
|
|
251
|
+
): Promise<T> {
|
|
252
|
+
type Tx = DuckDBTransactionWithInternals<TFullSchema, TSchema>;
|
|
253
|
+
const nestedTx = new DuckDBTransaction<TFullSchema, TSchema>(
|
|
254
|
+
(this as unknown as Tx).dialect,
|
|
255
|
+
(this as unknown as Tx).session,
|
|
256
|
+
this.schema,
|
|
257
|
+
this.nestedIndex + 1
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
return transaction(nestedTx);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export type GenericRowData<T extends RowData = RowData> = T;
|
|
265
|
+
|
|
266
|
+
export type GenericTableData<T = RowData> = T[];
|
|
267
|
+
|
|
268
|
+
const arrayLiteralWarning =
|
|
269
|
+
'Received a stringified Postgres-style array literal. Use duckDbList()/duckDbArray() or pass native arrays instead. You can also set rejectStringArrayLiterals=true to throw.';
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
export interface DuckDBQueryResultHKT extends PgQueryResultHKT {
|
|
273
|
+
type: GenericTableData<Assume<this['row'], RowData>>;
|
|
274
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
const selectionRegex = /select\s+(.+)\s+from/i;
|
|
2
|
+
const tableIdPropSelectionRegex = new RegExp(
|
|
3
|
+
[
|
|
4
|
+
`("(.+)"\\."(.+)")`, // table identifier + property
|
|
5
|
+
`(\\s+as\\s+'?(.+?)'?\\.'?(.+?)'?)?`, // optional AS clause
|
|
6
|
+
].join(''),
|
|
7
|
+
'i'
|
|
8
|
+
);
|
|
9
|
+
const noTableIdPropSelectionRegex = /"(.+)"(\s+as\s+'?\1'?)?/i;
|
|
10
|
+
|
|
11
|
+
export function adaptArrayOperators(query: string): string {
|
|
12
|
+
type ArrayOperator = {
|
|
13
|
+
token: '@>' | '<@' | '&&';
|
|
14
|
+
fn: 'array_has_all' | 'array_has_any';
|
|
15
|
+
swap?: boolean;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const operators: ArrayOperator[] = [
|
|
19
|
+
{ token: '@>', fn: 'array_has_all' },
|
|
20
|
+
{ token: '<@', fn: 'array_has_all', swap: true },
|
|
21
|
+
{ token: '&&', fn: 'array_has_any' },
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
const isWhitespace = (char: string | undefined) =>
|
|
25
|
+
char !== undefined && /\s/.test(char);
|
|
26
|
+
|
|
27
|
+
const walkLeft = (source: string, start: number): [number, string] => {
|
|
28
|
+
let idx = start;
|
|
29
|
+
while (idx >= 0 && isWhitespace(source[idx])) {
|
|
30
|
+
idx--;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let depth = 0;
|
|
34
|
+
let inString = false;
|
|
35
|
+
for (; idx >= 0; idx--) {
|
|
36
|
+
const ch = source[idx];
|
|
37
|
+
if (ch === "'" && source[idx - 1] !== '\\') {
|
|
38
|
+
inString = !inString;
|
|
39
|
+
}
|
|
40
|
+
if (inString) continue;
|
|
41
|
+
if (ch === ')' || ch === ']') {
|
|
42
|
+
depth++;
|
|
43
|
+
} else if (ch === '(' || ch === '[') {
|
|
44
|
+
depth--;
|
|
45
|
+
if (depth < 0) {
|
|
46
|
+
return [idx + 1, source.slice(idx + 1, start + 1)];
|
|
47
|
+
}
|
|
48
|
+
} else if (depth === 0 && isWhitespace(ch)) {
|
|
49
|
+
return [idx + 1, source.slice(idx + 1, start + 1)];
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return [0, source.slice(0, start + 1)];
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const walkRight = (source: string, start: number): [number, string] => {
|
|
56
|
+
let idx = start;
|
|
57
|
+
while (idx < source.length && isWhitespace(source[idx])) {
|
|
58
|
+
idx++;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let depth = 0;
|
|
62
|
+
let inString = false;
|
|
63
|
+
for (; idx < source.length; idx++) {
|
|
64
|
+
const ch = source[idx];
|
|
65
|
+
if (ch === "'" && source[idx - 1] !== '\\') {
|
|
66
|
+
inString = !inString;
|
|
67
|
+
}
|
|
68
|
+
if (inString) continue;
|
|
69
|
+
if (ch === '(' || ch === '[') {
|
|
70
|
+
depth++;
|
|
71
|
+
} else if (ch === ')' || ch === ']') {
|
|
72
|
+
depth--;
|
|
73
|
+
if (depth < 0) {
|
|
74
|
+
return [idx, source.slice(start, idx)];
|
|
75
|
+
}
|
|
76
|
+
} else if (depth === 0 && isWhitespace(ch)) {
|
|
77
|
+
return [idx, source.slice(start, idx)];
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return [source.length, source.slice(start)];
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
let rewritten = query;
|
|
84
|
+
for (const { token, fn, swap } of operators) {
|
|
85
|
+
let idx = rewritten.indexOf(token);
|
|
86
|
+
while (idx !== -1) {
|
|
87
|
+
const [leftStart, leftExpr] = walkLeft(rewritten, idx - 1);
|
|
88
|
+
const [rightEnd, rightExpr] = walkRight(
|
|
89
|
+
rewritten,
|
|
90
|
+
idx + token.length
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
const left = leftExpr.trim();
|
|
94
|
+
const right = rightExpr.trim();
|
|
95
|
+
|
|
96
|
+
const replacement = `${fn}(${swap ? right : left}, ${
|
|
97
|
+
swap ? left : right
|
|
98
|
+
})`;
|
|
99
|
+
|
|
100
|
+
rewritten =
|
|
101
|
+
rewritten.slice(0, leftStart) +
|
|
102
|
+
replacement +
|
|
103
|
+
rewritten.slice(rightEnd);
|
|
104
|
+
|
|
105
|
+
idx = rewritten.indexOf(token, leftStart + replacement.length);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return rewritten;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function queryAdapter(query: string): string {
|
|
113
|
+
const selection = selectionRegex.exec(query);
|
|
114
|
+
|
|
115
|
+
if (selection?.length !== 2) {
|
|
116
|
+
return query;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const fields = selection[1]
|
|
120
|
+
.split(',')
|
|
121
|
+
.map((field) => {
|
|
122
|
+
const trimmedField = field.trim();
|
|
123
|
+
const tableProp = tableIdPropSelectionRegex.exec(trimmedField);
|
|
124
|
+
if (tableProp) {
|
|
125
|
+
const [, identifier, table, column, , aliasTable, aliasColumn] =
|
|
126
|
+
tableProp;
|
|
127
|
+
|
|
128
|
+
const asAlias = `'${aliasTable ?? table}.${aliasColumn ?? column}'`;
|
|
129
|
+
if (tableProp[4]) {
|
|
130
|
+
return trimmedField.replace(tableProp[4], ` as ${asAlias}`);
|
|
131
|
+
}
|
|
132
|
+
return `${identifier} as ${asAlias}`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const noTableProp = noTableIdPropSelectionRegex.exec(trimmedField);
|
|
136
|
+
if (noTableProp) {
|
|
137
|
+
const [, column, alias] = noTableProp;
|
|
138
|
+
const asAlias = ` as '${column}'`;
|
|
139
|
+
return alias ? trimmedField.replace(alias, asAlias) : `${trimmedField}${asAlias}`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return trimmedField;
|
|
143
|
+
})
|
|
144
|
+
.filter(Boolean) as string[];
|
|
145
|
+
|
|
146
|
+
return query.replace(selection[1], fields.join(', '));
|
|
147
|
+
}
|