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

Feature/add bson column type #8

Draft
wants to merge 8 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions drizzle-kit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
"@vercel/postgres": "^0.8.0",
"ava": "^5.1.0",
"better-sqlite3": "^9.4.3",
"bson": "^6.8.0",
"camelcase": "^7.0.1",
"chalk": "^5.2.0",
"commander": "^12.1.0",
Expand Down
30 changes: 12 additions & 18 deletions drizzle-kit/src/introspect-singlestore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ const singlestoreImportsList = new Set([
'singlestoreEnum',
'bigint',
'binary',
// TODO: add new type Blob
'boolean',
'bson',
'char',
'date',
'datetime',
Expand All @@ -21,8 +23,6 @@ const singlestoreImportsList = new Set([
'float',
'int',
'json',
// TODO: add new type BSON
// TODO: add new type Blob
// TODO: add new type UUID
// TODO: add new type GUID
// TODO: add new type Vector
Expand Down Expand Up @@ -245,18 +245,6 @@ const mapColumnDefault = (defaultValue: any, isExpression?: boolean) => {
return defaultValue;
};

const mapColumnDefaultForJson = (defaultValue: any) => {
if (
typeof defaultValue === 'string'
&& defaultValue.startsWith("('")
&& defaultValue.endsWith("')")
) {
return defaultValue.substring(2, defaultValue.length - 2);
}

return defaultValue;
};

const column = (
type: string,
name: string,
Expand Down Expand Up @@ -490,19 +478,25 @@ const column = (
return out;
}

// in mysql json can't have default value. Will leave it in case smth ;)
// TODO: check if SingleStore has json can't have default value
if (lowered === 'json') {
let out = `${casing(name)}: json("${name}")`;

out += defaultValue
? `.default(${mapColumnDefaultForJson(defaultValue)})`
? `.default(${mapColumnDefault(defaultValue)})`
: '';

return out;
}

// TODO: add new type BSON
if (lowered === 'bson') {
let out = `${casing(name)}: bson("${name}")`;

out += defaultValue
? `.default(${mapColumnDefault(defaultValue)})`
: '';

return out;
}

// TODO: add new type Blob

Expand Down
6 changes: 6 additions & 0 deletions drizzle-kit/src/serializer/singlestoreSerializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { RowDataPacket } from 'mysql2/promise';
import { withStyle } from '../cli/validations/outputs';
import { IntrospectStage, IntrospectStatus } from '../cli/views';

import { BSON } from 'bson';
import type { DB } from '../utils';
import { sqlToStr } from '.';
import {
Expand Down Expand Up @@ -130,6 +131,11 @@ export const generateSingleStoreSnapshot = (
} else {
if (sqlTypeLowered === 'json') {
columnToSet.default = `'${JSON.stringify(column.default)}'`;
} else if (sqlTypeLowered === 'bson') {
if (column.default !== null) {
const hexaCode = `0x${Buffer.from(BSON.serialize(column.default)).toString('hex')}`;
columnToSet.default = `${hexaCode}`;
}
} else if (column.default instanceof Date) {
if (sqlTypeLowered === 'date') {
columnToSet.default = `'${column.default.toISOString().split('T')[0]}'`;
Expand Down
45 changes: 45 additions & 0 deletions drizzle-kit/tests/singlestore.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { BSON } from 'bson';
import { sql } from 'drizzle-orm';
import {
bson,
index,
json,
primaryKey,
Expand Down Expand Up @@ -495,6 +497,49 @@ test('add table #14', async () => {
});

// TODO: add bson type tests
test('add table with bson field', async () => {
const to = {
users: singlestoreTable('table', {
bson: bson('bson'),
}),
};

const { sqlStatements } = await diffTestSchemasSingleStore({}, to, []);
expect(sqlStatements.length).toBe(1);
expect(sqlStatements[0]).toBe('CREATE TABLE `table` (\n\t`bson` bson\n);\n');
});

test('add table with bson field with empty default value', async () => {
const to = {
users: singlestoreTable('table', {
bson: bson('bson').default({}),
}),
};

const hexaCode = `0x${Buffer.from(BSON.serialize({})).toString('hex')}`;

const { sqlStatements } = await diffTestSchemasSingleStore({}, to, []);
expect(sqlStatements.length).toBe(1);
expect(sqlStatements[0]).toBe(
`CREATE TABLE \`table\` (\n\t\`bson\` bson DEFAULT ${hexaCode}\n);\n`,
);
});

test('add table with bson field with object default value', async () => {
const to = {
users: singlestoreTable('table', {
bson: bson('bson').default({ key: 'value' }),
}),
};

const hexaCode = `0x${Buffer.from(BSON.serialize({ key: 'value' })).toString('hex')}`;

const { sqlStatements } = await diffTestSchemasSingleStore({}, to, []);
expect(sqlStatements.length).toBe(1);
expect(sqlStatements[0]).toBe(
`CREATE TABLE \`table\` (\n\t\`bson\` bson DEFAULT ${hexaCode}\n);\n`,
);
});

// TODO: add blob type tests

Expand Down
1 change: 1 addition & 0 deletions drizzle-orm/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@
"@vercel/postgres": "^0.8.0",
"@xata.io/client": "^0.29.3",
"better-sqlite3": "^8.4.0",
"bson": "^6.8.0",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm curious about how we want to represent the data here. Do we expect users to be dealing with BSON data in a manner like this, or are we expecting them to convert it to JSON anyways? Especially considering the to-driver stuff is using JSON.stringify

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From my perspective, the ideal scenario is that users deal only with JSON strings and let the form deal with the translation. That would make things more easy for users.

Something like:

db.singlestoreTable({
  ...
   bson_column: bson().notnull().default({"key": "value"})
   ...
 }) 

Currently there is a bug on the translation of literals into BSON on the engine side, so this will only be possible once the bug on the engine is fixed. For now the default value has to be converted to BSON object and then to hexa string

More info about the bug on slack: https://memsql.slack.com/archives/C02G51BAJ/p1724069568691219?thread_ts=1724061645.083329&cid=C02G51BAJ

I also found out that you can cast json literals on select and insert statements, but not on create table statements. Because of that, we can keep the jsonString:>BSON on the mapToDriver return value, since the drizzle-orm package only deals with insert and select operations, and the option above on drizzle-kit, that deals with scheam modifications (create table and create/alter column)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that all makes sense from the engine side. I guess I was more curious about the JS side of when we get the data out from the engine. Do we expect users to use BSON directly in TypeScript or should we also convert it back to JSON?

"bun-types": "^0.6.6",
"cpy": "^10.1.0",
"expo-sqlite": "^13.2.0",
Expand Down
15 changes: 7 additions & 8 deletions drizzle-orm/src/singlestore-core/columns/bson.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,25 @@ import type { ColumnBuilderBaseConfig, ColumnBuilderRuntimeConfig, MakeColumnCon
import type { ColumnBaseConfig } from '~/column.ts';
import { entityKind } from '~/entity.ts';
import type { AnySingleStoreTable } from '~/singlestore-core/table.ts';
import { sql } from '~/sql/sql.ts';
import { SingleStoreColumn, SingleStoreColumnBuilder } from './common.ts';

export type SingleStoreBsonBuilderInitial<TName extends string> = SingleStoreBsonBuilder<{
name: TName;
dataType: 'json'; // The bson is stored as a json string the same way binary is stored as a string (check `./binary.ts`)
dataType: 'buffer';
columnType: 'SingleStoreBson';
data: unknown;
driverParam: string;
enumValues: undefined;
generated: undefined;
}>;

export class SingleStoreBsonBuilder<T extends ColumnBuilderBaseConfig<'json', 'SingleStoreBson'>>
export class SingleStoreBsonBuilder<T extends ColumnBuilderBaseConfig<'buffer', 'SingleStoreBson'>>
extends SingleStoreColumnBuilder<T>
{
static readonly [entityKind]: string = 'SingleStoreBsonBuilder';

constructor(name: T['name']) {
super(name, 'json', 'SingleStoreBson');
super(name, 'buffer', 'SingleStoreBson');
}

/** @internal */
Expand All @@ -35,16 +34,16 @@ export class SingleStoreBsonBuilder<T extends ColumnBuilderBaseConfig<'json', 'S
}
}

export class SingleStoreBson<T extends ColumnBaseConfig<'json', 'SingleStoreBson'>> extends SingleStoreColumn<T> {
export class SingleStoreBson<T extends ColumnBaseConfig<'buffer', 'SingleStoreBson'>> extends SingleStoreColumn<T> {
static readonly [entityKind]: string = 'SingleStoreBson';

getSQLType(): string {
return 'bson';
}

override mapToDriverValue(value: T['data']) {
const json = JSON.stringify(value);
return sql`${json}:>BSON`;
override mapToDriverValue(value: T['data']): string {
const jsonData = JSON.stringify(value);
return `${jsonData}:>BSON`;
}
}

Expand Down
1 change: 1 addition & 0 deletions drizzle-orm/src/singlestore-core/columns/json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export class SingleStoreJson<T extends ColumnBaseConfig<'json', 'SingleStoreJson
}

override mapToDriverValue(value: T['data']): string {
console.log('value', value);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: rm log

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice catch 👍

return JSON.stringify(value);
}
}
Expand Down
84 changes: 84 additions & 0 deletions drizzle-orm/src/singlestore-core/query-builders/select.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1028,6 +1028,48 @@ export const unionAll = createSetOperator('union', true);
*/
export const intersect = createSetOperator('intersect', false);

/**
* Adds `intersect all` set operator to the query.
*
* Calling this method will retain only the rows that are present in both result sets including all duplicates.
*
* See docs: {@link https://orm.drizzle.team/docs/set-operations#intersect-all}
*
* @example
*
* ```ts
* // Select all products and quantities that are ordered by both regular and VIP customers
* await db.select({
* productId: regularCustomerOrders.productId,
* quantityOrdered: regularCustomerOrders.quantityOrdered
* })
* .from(regularCustomerOrders)
* .intersectAll(
* db.select({
* productId: vipCustomerOrders.productId,
* quantityOrdered: vipCustomerOrders.quantityOrdered
* })
* .from(vipCustomerOrders)
* );
* // or
* import { intersectAll } from 'drizzle-orm/mysql-core'
*
* await intersectAll(
* db.select({
* productId: regularCustomerOrders.productId,
* quantityOrdered: regularCustomerOrders.quantityOrdered
* })
* .from(regularCustomerOrders),
* db.select({
* productId: vipCustomerOrders.productId,
* quantityOrdered: vipCustomerOrders.quantityOrdered
* })
* .from(vipCustomerOrders)
* );
* ```
*/
export const intersectAll = createSetOperator('intersect', true);

/**
* Adds `except` set operator to the query.
*
Expand Down Expand Up @@ -1055,6 +1097,48 @@ export const intersect = createSetOperator('intersect', false);
*/
export const except = createSetOperator('except', false);

/**
* Adds `except all` set operator to the query.
*
* Calling this method will retrieve all rows from the left query, except for the rows that are present in the result set of the right query.
*
* See docs: {@link https://orm.drizzle.team/docs/set-operations#except-all}
*
* @example
*
* ```ts
* // Select all products that are ordered by regular customers but not by VIP customers
* await db.select({
* productId: regularCustomerOrders.productId,
* quantityOrdered: regularCustomerOrders.quantityOrdered,
* })
* .from(regularCustomerOrders)
* .exceptAll(
* db.select({
* productId: vipCustomerOrders.productId,
* quantityOrdered: vipCustomerOrders.quantityOrdered,
* })
* .from(vipCustomerOrders)
* );
* // or
* import { exceptAll } from 'drizzle-orm/mysql-core'
*
* await exceptAll(
* db.select({
* productId: regularCustomerOrders.productId,
* quantityOrdered: regularCustomerOrders.quantityOrdered
* })
* .from(regularCustomerOrders),
* db.select({
* productId: vipCustomerOrders.productId,
* quantityOrdered: vipCustomerOrders.quantityOrdered
* })
* .from(vipCustomerOrders)
* );
* ```
*/
export const exceptAll = createSetOperator('except', true);

/**
* Adds `minus` set operator to the query.
*
Expand Down
Loading
Loading