Skip to content

Commit

Permalink
Merge pull request #61 from outerbase/invisal/support-raw-placeholders
Browse files Browse the repository at this point in the history
support raw based holder
  • Loading branch information
Brayden authored Nov 21, 2024
2 parents 40ce6e8 + 86ddce5 commit 2d40411
Show file tree
Hide file tree
Showing 14 changed files with 369 additions and 24 deletions.
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

0 comments on commit 2d40411

Please sign in to comment.