Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

support raw based holder #61

Merged
merged 11 commits into from
Nov 21, 2024
Merged
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@outerbase/sdk",
"version": "2.0.0-rc.2",
"version": "2.0.0-rc.3",
"description": "",
"main": "dist/index.js",
"module": "dist/index.js",
Expand Down
2 changes: 1 addition & 1 deletion src/connections/bigquery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ export class BigQueryConnection extends SqlConnection {
* @param parameters - An object containing the parameters to be used in the query.
* @returns Promise<{ data: any, error: Error | null }>
*/
async query<T = Record<string, unknown>>(
async internalQuery<T = Record<string, unknown>>(
query: Query
): Promise<QueryResult<T>> {
try {
Expand Down
5 changes: 4 additions & 1 deletion src/connections/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@ export abstract class Connection {

// Retrieve metadata about the database, useful for introspection.
abstract fetchDatabaseSchema(): Promise<Database>;
abstract raw(query: string): Promise<QueryResult>;
abstract raw(
query: string,
params?: Record<string, unknown> | unknown[]
): Promise<QueryResult>;
abstract testConnection(): Promise<{ error?: string }>;

// Connection common operations that will be used by Outerbase
Expand Down
2 changes: 1 addition & 1 deletion src/connections/motherduck.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export class DuckDBConnection extends PostgreBaseConnection {
* @param parameters - An object containing the parameters to be used in the query.
* @returns Promise<{ data: any, error: Error | null }>
*/
async query<T = Record<string, unknown>>(
async internalQuery<T = Record<string, unknown>>(
query: Query
): Promise<QueryResult<T>> {
const connection = this.connection;
Expand Down
2 changes: 1 addition & 1 deletion src/connections/mysql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ export class MySQLConnection extends SqlConnection {
return super.mapDataType(dataType);
}

async query<T = Record<string, unknown>>(
async internalQuery<T = Record<string, unknown>>(
query: Query
): Promise<QueryResult<T>> {
try {
Expand Down
15 changes: 3 additions & 12 deletions src/connections/postgre/postgresql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,16 @@ import { QueryResult } from '..';
import { Query } from '../../query';
import { AbstractDialect } from './../../query-builder';
import { PostgresDialect } from './../../query-builder/dialects/postgres';
import { QueryType } from './../../query-params';
import {
createErrorResult,
transformArrayBasedResult,
} from './../../utils/transformer';
import { PostgreBaseConnection } from './base';

function replacePlaceholders(query: string): string {
let index = 1;
return query.replace(/\?/g, () => `$${index++}`);
}

export class PostgreSQLConnection extends PostgreBaseConnection {
client: Client;
dialect: AbstractDialect = new PostgresDialect();
queryType: QueryType = QueryType.positional;
protected numberedPlaceholder = true;

constructor(pgClient: any) {
super();
Expand All @@ -33,15 +27,12 @@ export class PostgreSQLConnection extends PostgreBaseConnection {
await this.client.end();
}

async query<T = Record<string, unknown>>(
async internalQuery<T = Record<string, unknown>>(
query: Query
): Promise<QueryResult<T>> {
try {
const { rows, fields } = await this.client.query({
text:
query.parameters?.length === 0
? query.query
: replacePlaceholders(query.query),
text: query.query,
rowMode: 'array',
values: query.parameters as unknown[],
});
Expand Down
2 changes: 1 addition & 1 deletion src/connections/snowflake/snowflake.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ export class SnowflakeConnection extends PostgreBaseConnection {
);
}

async query<T = Record<string, unknown>>(
async internalQuery<T = Record<string, unknown>>(
query: Query
): Promise<QueryResult<T>> {
try {
Expand Down
59 changes: 56 additions & 3 deletions src/connections/sql-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,16 @@ import {
} from '..';
import { AbstractDialect, ColumnDataType } from './../query-builder';
import { TableColumn, TableColumnDefinition } from './../models/database';
import {
namedPlaceholder,
toNumberedPlaceholders,
} from './../utils/placeholder';

export abstract class SqlConnection extends Connection {
abstract dialect: AbstractDialect;
protected numberedPlaceholder = false;

abstract query<T = Record<string, unknown>>(
abstract internalQuery<T = Record<string, unknown>>(
query: Query
): Promise<QueryResult<T>>;

Expand All @@ -21,8 +26,56 @@ export abstract class SqlConnection extends Connection {
return dataType;
}

async raw(query: string): Promise<QueryResult> {
return await this.query({ query });
/**
* This is a deprecated function, use raw instead. We keep this for
* backward compatibility.
*
* @deprecated
* @param query
* @returns
*/
async query<T = Record<string, unknown>>(
query: Query
): Promise<QueryResult<T>> {
return (await this.raw(
query.query,
query.parameters
)) as QueryResult<T>;
}

async raw(
query: string,
params?: Record<string, unknown> | unknown[]
): Promise<QueryResult> {
if (!params) return await this.internalQuery({ query });

// Positional placeholder
if (Array.isArray(params)) {
if (this.numberedPlaceholder) {
const { query: newQuery, bindings } = toNumberedPlaceholders(
query,
params
);

return await this.internalQuery({
query: newQuery,
parameters: bindings,
});
}

return await this.internalQuery({ query, parameters: params });
}

// Named placeholder
const { query: newQuery, bindings } = namedPlaceholder(
query,
params!,
this.numberedPlaceholder
);
return await this.internalQuery({
query: newQuery,
parameters: bindings,
});
}

async select(
Expand Down
2 changes: 1 addition & 1 deletion src/connections/sqlite/cloudflare.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ export class CloudflareD1Connection extends SqliteBaseConnection {
* @param parameters - An object containing the parameters to be used in the query.
* @returns Promise<{ data: any, error: Error | null }>
*/
async query<T = Record<string, unknown>>(
async internalQuery<T = Record<string, unknown>>(
query: Query
): Promise<QueryResult<T>> {
if (!this.apiKey) throw new Error('Cloudflare API key is not set');
Expand Down
2 changes: 1 addition & 1 deletion src/connections/sqlite/starbase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export class StarbaseConnection extends SqliteBaseConnection {
* @param parameters - An object containing the parameters to be used in the query.
* @returns Promise<{ data: any, error: Error | null }>
*/
async query<T = Record<string, unknown>>(
async internalQuery<T = Record<string, unknown>>(
query: Query
): Promise<QueryResult<T>> {
if (!this.url) throw new Error('Starbase URL is not set');
Expand Down
2 changes: 1 addition & 1 deletion src/connections/sqlite/turso.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export class TursoConnection extends SqliteBaseConnection {
this.client = client;
}

async query<T = Record<string, unknown>>(
async internalQuery<T = Record<string, unknown>>(
query: Query
): Promise<QueryResult<T>> {
try {
Expand Down
157 changes: 157 additions & 0 deletions src/utils/placeholder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
const RE_PARAM = /(?:\?)|(?::(\d+|(?:[a-zA-Z][a-zA-Z0-9_]*)))/g,
DQUOTE = 34,
SQUOTE = 39,
BSLASH = 92;

/**
* This code is based on https://github.com/mscdex/node-mariasql/blob/master/lib/Client.js#L296-L420
* License: https://github.com/mscdex/node-mariasql/blob/master/LICENSE
*
* @param query
* @returns
*/
function parse(query: string): [string] | [string[], (string | number)[]] {
let ppos = RE_PARAM.exec(query);
let curpos = 0;
let start = 0;
let end;
const parts = [];
let inQuote = false;
let escape = false;
let qchr;
const tokens = [];
let qcnt = 0;
let lastTokenEndPos = 0;
let i;

if (ppos) {
do {
for (i = curpos, end = ppos.index; i < end; ++i) {
let chr = query.charCodeAt(i);
if (chr === BSLASH) escape = !escape;
else {
if (escape) {
escape = false;
continue;
}
if (inQuote && chr === qchr) {
if (query.charCodeAt(i + 1) === qchr) {
// quote escaped via "" or ''
++i;
continue;
}
inQuote = false;
} else if (!inQuote && (chr === DQUOTE || chr === SQUOTE)) {
inQuote = true;
qchr = chr;
}
}
}
if (!inQuote) {
parts.push(query.substring(start, end));
tokens.push(ppos[0].length === 1 ? qcnt++ : ppos[1]);
start = end + ppos[0].length;
lastTokenEndPos = start;
}
curpos = end + ppos[0].length;
} while ((ppos = RE_PARAM.exec(query)));

if (tokens.length) {
if (curpos < query.length) {
parts.push(query.substring(lastTokenEndPos));
}
return [parts, tokens];
}
}
return [query];
}

export function namedPlaceholder(
query: string,
params: Record<string, unknown>,
numbered = false
): { query: string; bindings: unknown[] } {
const parts = parse(query);

if (parts.length === 1) {
return { query, bindings: [] };
}

const bindings = [];
let newQuery = '';

const [sqlFragments, placeholders] = parts;

// If placeholders contains any number, then it's a mix of named and numbered placeholders
if (placeholders.some((p) => typeof p === 'number')) {
throw new Error(
'Mixing named and positional placeholder should throw error'
);
}

for (let i = 0; i < sqlFragments.length; i++) {
newQuery += sqlFragments[i];

if (i < placeholders.length) {
const key = placeholders[i];

if (numbered) {
newQuery += `$${i + 1}`;
} else {
newQuery += `?`;
}

const placeholderValue = params[key];
if (placeholderValue === undefined) {
throw new Error(`Missing value for placeholder ${key}`);
}

bindings.push(params[key]);
}
}

return { query: newQuery, bindings };
}

export function toNumberedPlaceholders(
query: string,
params: unknown[]
): {
query: string;
bindings: unknown[];
} {
const parts = parse(query);

if (parts.length === 1) {
return { query, bindings: [] };
}

const bindings = [];
let newQuery = '';

const [sqlFragments, placeholders] = parts;

if (placeholders.length !== params.length) {
throw new Error(
'Number of positional placeholder should match with the number of values'
);
}

// Mixing named and numbered placeholders should throw error
if (placeholders.some((p) => typeof p === 'string')) {
throw new Error(
'Mixing named and positional placeholder should throw error'
);
}

for (let i = 0; i < sqlFragments.length; i++) {
newQuery += sqlFragments[i];

if (i < placeholders.length) {
newQuery += `$${i + 1}`;
bindings.push(params[i]);
}
}

return { query: newQuery, bindings };
}
Loading