diff --git a/drizzle-kit/src/introspect-singlestore.ts b/drizzle-kit/src/introspect-singlestore.ts index 09c2feec0..291c079e3 100644 --- a/drizzle-kit/src/introspect-singlestore.ts +++ b/drizzle-kit/src/introspect-singlestore.ts @@ -30,8 +30,8 @@ const singlestoreImportsList = new Set([ 'float', 'int', 'json', + 'blob', // 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 @@ -789,6 +789,12 @@ const column = ( return out; } + if (lowered === 'blob') { + let out = `${casing(name)}: blob(${dbColumnName({ name, casing: rawCasing })})`; + out += defaultValue ? `.default(${mapColumnDefault(defaultValue)})` : ''; + return out; + } + console.log('uknown', type); return `// Warning: Can't parse ${type} from database\n\t// ${type}Type: ${type}("${name}")`; }; diff --git a/drizzle-kit/src/serializer/singlestoreSerializer.ts b/drizzle-kit/src/serializer/singlestoreSerializer.ts index e8c89f1d1..738746120 100644 --- a/drizzle-kit/src/serializer/singlestoreSerializer.ts +++ b/drizzle-kit/src/serializer/singlestoreSerializer.ts @@ -129,6 +129,8 @@ export const generateSingleStoreSnapshot = ( } else { if (typeof column.default === 'string') { columnToSet.default = `'${column.default}'`; + } else if (typeof column.default === 'bigint') { + columnToSet.default = Number(column.default); } else { if (sqlTypeLowered === 'json') { columnToSet.default = `'${JSON.stringify(column.default)}'`; @@ -150,9 +152,6 @@ export const generateSingleStoreSnapshot = ( columnToSet.default = column.default; } } - // if (['blob', 'text', 'json'].includes(column.getSQLType())) { - // columnToSet.default = `(${columnToSet.default})`; - // } } } columnsObject[column.name] = columnToSet; diff --git a/drizzle-kit/tests/push/singlestore.test.ts b/drizzle-kit/tests/push/singlestore.test.ts index 82c72063c..9a78866b5 100644 --- a/drizzle-kit/tests/push/singlestore.test.ts +++ b/drizzle-kit/tests/push/singlestore.test.ts @@ -3,6 +3,7 @@ import { SQL, sql } from 'drizzle-orm'; import { bigint, binary, + blob, char, date, datetime, @@ -249,6 +250,14 @@ const singlestoreSuite: DialectSuite = { columnNotNull: binary('column_not_null', { length: 1 }).notNull(), columnDefault: binary('column_default', { length: 12 }), }), + allBlobs: singlestoreTable('all_blobs', { + bigIntSimple: blob('big_int_simple', { mode: 'bigint' }), + bigIntColumnNotNull: blob('big_int_column_not_null', { mode: 'bigint' }).notNull(), + bigIntColumnDefault: blob('big_int_column_default', { mode: 'bigint' }).default(BigInt(12)), + jsonSimple: blob('json_simple', { mode: 'json' }), + jsonColumnNotNull: blob('json_column_not_null', { mode: 'json' }).notNull(), + jsonColumnDefault: blob('json_column_default', { mode: 'json' }).default('{"hello":"world"}'), + }), }; const { statements } = await diffTestSchemasPushSingleStore( @@ -259,7 +268,6 @@ const singlestoreSuite: DialectSuite = { 'drizzle', false, ); - console.log(statements); expect(statements.length).toBe(0); expect(statements).toEqual([]); diff --git a/drizzle-orm/src/singlestore-core/columns/all.ts b/drizzle-orm/src/singlestore-core/columns/all.ts index 66d289e3f..c35cd1fce 100644 --- a/drizzle-orm/src/singlestore-core/columns/all.ts +++ b/drizzle-orm/src/singlestore-core/columns/all.ts @@ -1,5 +1,6 @@ import { bigint } from './bigint.ts'; import { binary } from './binary.ts'; +import { blob } from './blob.ts'; import { boolean } from './boolean.ts'; import { char } from './char.ts'; import { customType } from './custom.ts'; @@ -25,6 +26,7 @@ import { year } from './year.ts'; export function getSingleStoreColumnBuilders() { return { + blob, bigint, binary, boolean, diff --git a/drizzle-orm/src/singlestore-core/columns/blob.ts b/drizzle-orm/src/singlestore-core/columns/blob.ts new file mode 100644 index 000000000..f8125602a --- /dev/null +++ b/drizzle-orm/src/singlestore-core/columns/blob.ts @@ -0,0 +1,187 @@ +import type { ColumnBuilderBaseConfig, ColumnBuilderRuntimeConfig, MakeColumnConfig } from '~/column-builder.ts'; +import type { ColumnBaseConfig } from '~/column.ts'; +import { entityKind } from '~/entity.ts'; +import type { AnySingleStoreTable } from '~/singlestore-core/table.ts'; +import { type Equal, getColumnNameAndConfig } from '~/utils.ts'; +import { SingleStoreColumn, SingleStoreColumnBuilder } from './common.ts'; + +type BlobMode = 'buffer' | 'json' | 'bigint'; + +export type SingleStoreBigIntBuilderInitial = SingleStoreBigIntBuilder<{ + name: TName; + dataType: 'bigint'; + columnType: 'SingleStoreBigInt'; + data: bigint; + driverParam: Buffer; + enumValues: undefined; +}>; + +export class SingleStoreBigIntBuilder> + extends SingleStoreColumnBuilder +{ + static override readonly [entityKind]: string = 'SingleStoreBigIntBuilder'; + + constructor(name: T['name']) { + super(name, 'bigint', 'SingleStoreBigInt'); + } + + /** @internal */ + override build( + table: AnySingleStoreTable<{ name: TTableName }>, + ): SingleStoreBigInt> { + return new SingleStoreBigInt>( + table, + this.config as ColumnBuilderRuntimeConfig, + ); + } +} + +export class SingleStoreBigInt> extends SingleStoreColumn { + static override readonly [entityKind]: string = 'SingleStoreBigInt'; + + getSQLType(): string { + return 'blob'; + } + + override mapFromDriverValue(value: Buffer | Uint8Array): bigint { + if (Buffer.isBuffer(value)) { + return BigInt(value.toString()); + } + + return BigInt(String.fromCodePoint(...value)); + } + + override mapToDriverValue(value: bigint): string { + return value.toString(); + } +} + +export type SingleStoreBlobJsonBuilderInitial = SingleStoreBlobJsonBuilder<{ + name: TName; + dataType: 'json'; + columnType: 'SingleStoreBlobJson'; + data: unknown; + driverParam: Buffer; + enumValues: undefined; +}>; + +export class SingleStoreBlobJsonBuilder> + extends SingleStoreColumnBuilder +{ + static override readonly [entityKind]: string = 'SingleStoreBlobJsonBuilder'; + + constructor(name: T['name']) { + super(name, 'json', 'SingleStoreBlobJson'); + } + + /** @internal */ + override build( + table: AnySingleStoreTable<{ name: TTableName }>, + ): SingleStoreBlobJson> { + return new SingleStoreBlobJson>( + table, + this.config as ColumnBuilderRuntimeConfig, + ); + } +} + +export class SingleStoreBlobJson> + extends SingleStoreColumn +{ + static override readonly [entityKind]: string = 'SingleStoreBlobJson'; + + getSQLType(): string { + return 'blob'; + } + + override mapFromDriverValue(value: Buffer | Uint8Array | ArrayBuffer): T['data'] { + if (Buffer.isBuffer(value)) { + return JSON.parse(value.toString()); + } + + // for sqlite durable objects + // eslint-disable-next-line no-instanceof/no-instanceof + if (value instanceof ArrayBuffer) { + const decoder = new TextDecoder(); + return JSON.parse(decoder.decode(value)); + } + + return JSON.parse(String.fromCodePoint(...value)); + } + + override mapToDriverValue(value: T['data']): string { + return JSON.stringify(value); + } +} + +export type SingleStoreBlobBufferBuilderInitial = SingleStoreBlobBufferBuilder<{ + name: TName; + dataType: 'buffer'; + columnType: 'SingleStoreBlobBuffer'; + data: Buffer; + driverParam: Buffer; + enumValues: undefined; +}>; + +export class SingleStoreBlobBufferBuilder> + extends SingleStoreColumnBuilder +{ + static override readonly [entityKind]: string = 'SingleStoreBlobBufferBuilder'; + + constructor(name: T['name']) { + super(name, 'buffer', 'SingleStoreBlobBuffer'); + } + + /** @internal */ + override build( + table: AnySingleStoreTable<{ name: TTableName }>, + ): SingleStoreBlobBuffer> { + return new SingleStoreBlobBuffer>( + table, + this.config as ColumnBuilderRuntimeConfig, + ); + } +} + +export class SingleStoreBlobBuffer> + extends SingleStoreColumn +{ + static override readonly [entityKind]: string = 'SingleStoreBlobBuffer'; + + getSQLType(): string { + return 'blob'; + } +} + +export interface BlobConfig { + mode: TMode; +} + +/** + * It's recommended to use `text('...', { mode: 'json' })` instead of `blob` in JSON mode, because it supports JSON functions: + * >All JSON functions currently throw an error if any of their arguments are BLOBs because BLOBs are reserved for a future enhancement in which BLOBs will store the binary encoding for JSON. + * + * https://www.sqlite.org/json1.html + */ +export function blob(): SingleStoreBlobJsonBuilderInitial<''>; +export function blob( + config?: BlobConfig, +): Equal extends true ? SingleStoreBigIntBuilderInitial<''> + : Equal extends true ? SingleStoreBlobBufferBuilderInitial<''> + : SingleStoreBlobJsonBuilderInitial<''>; +export function blob( + name: TName, + config?: BlobConfig, +): Equal extends true ? SingleStoreBigIntBuilderInitial + : Equal extends true ? SingleStoreBlobBufferBuilderInitial + : SingleStoreBlobJsonBuilderInitial; +export function blob(a?: string | BlobConfig, b?: BlobConfig) { + const { name, config } = getColumnNameAndConfig(a, b); + if (config?.mode === 'json') { + return new SingleStoreBlobJsonBuilder(name); + } + if (config?.mode === 'bigint') { + return new SingleStoreBigIntBuilder(name); + } + return new SingleStoreBlobBufferBuilder(name); +} diff --git a/drizzle-orm/src/singlestore-core/columns/index.ts b/drizzle-orm/src/singlestore-core/columns/index.ts index b51f0fac4..1c3ba71f3 100644 --- a/drizzle-orm/src/singlestore-core/columns/index.ts +++ b/drizzle-orm/src/singlestore-core/columns/index.ts @@ -1,5 +1,6 @@ export * from './bigint.ts'; export * from './binary.ts'; +export * from './blob.ts'; export * from './boolean.ts'; export * from './char.ts'; export * from './common.ts'; diff --git a/drizzle-orm/type-tests/singlestore/tables.ts b/drizzle-orm/type-tests/singlestore/tables.ts index 73d9c6993..33b2171bc 100644 --- a/drizzle-orm/type-tests/singlestore/tables.ts +++ b/drizzle-orm/type-tests/singlestore/tables.ts @@ -4,6 +4,7 @@ import { eq } from '~/expressions.ts'; import { bigint, binary, + blob, boolean, char, customType, @@ -836,6 +837,9 @@ Expect< { singlestoreTable('all_columns', { + blob: blob('blob'), + blob2: blob('blob2', { mode: 'bigint' }), + blobdef: blob('blobdef').default(0), bigint: bigint('bigint', { mode: 'number' }), bigint2: bigint('bigint', { mode: 'number', unsigned: true }), bigintdef: bigint('bigintdef', { mode: 'number' }).default(0), @@ -934,6 +938,9 @@ Expect< { singlestoreTable('all_columns_without_name', { + blob: blob(), + blob2: blob({ mode: 'bigint' }), + blobdef: blob().default(0), bigint: bigint({ mode: 'number' }), bigint2: bigint({ mode: 'number', unsigned: true }), bigintdef: bigint({ mode: 'number' }).default(0), diff --git a/integration-tests/tests/singlestore/singlestore-common.ts b/integration-tests/tests/singlestore/singlestore-common.ts index fe7c2afb4..f4ee5fae5 100644 --- a/integration-tests/tests/singlestore/singlestore-common.ts +++ b/integration-tests/tests/singlestore/singlestore-common.ts @@ -29,6 +29,7 @@ import type { SingleStoreDatabase } from 'drizzle-orm/singlestore-core'; import { alias, bigint, + blob, boolean, date, datetime, @@ -38,7 +39,6 @@ import { index, int, intersect, - json, mediumint, primaryKey, serial, @@ -83,7 +83,7 @@ const usersTable = singlestoreTable('userstest', { id: serial('id').primaryKey(), name: text('name').notNull(), verified: boolean('verified').notNull().default(false), - jsonb: json('jsonb').$type(), + json: blob('json', { mode: 'json' }).$type(), // very similar to the sqlite test createdAt: timestamp('created_at').notNull().defaultNow(), }); @@ -146,6 +146,12 @@ const usersMigratorTable = singlestoreTable('users12', { }; }); +const bigIntExample = singlestoreTable('big_int_example', { + id: serial('id').primaryKey(), + name: text('name').notNull(), + bigInt: blob('big_int', { mode: 'bigint' }).notNull(), +}); + // To test aggregate functions const aggregateTable = singlestoreTable('aggregate_table', { id: serial('id').notNull(), @@ -163,7 +169,7 @@ const usersMySchemaTable = mySchema.table('userstest', { id: serial('id').primaryKey(), name: text('name').notNull(), verified: boolean('verified').notNull().default(false), - jsonb: json('jsonb').$type(), + json: blob('json', { mode: 'json' }).$type(), // very similar to the sqlite test createdAt: timestamp('created_at').notNull().defaultNow(), }); @@ -221,6 +227,7 @@ export function tests(driver?: string) { await db.execute(sql`drop table if exists userstest`); await db.execute(sql`drop table if exists users2`); await db.execute(sql`drop table if exists cities`); + await db.execute(sql`drop table if exists ${bigIntExample}`); await db.execute(sql`drop schema if exists \`mySchema\``); await db.execute(sql`create schema if not exists \`mySchema\``); @@ -231,7 +238,7 @@ export function tests(driver?: string) { id serial primary key, name text not null, verified boolean not null default false, - jsonb json, + json blob, created_at timestamp not null default now() ) `, @@ -263,7 +270,7 @@ export function tests(driver?: string) { \`id\` serial primary key, \`name\` text not null, \`verified\` boolean not null default false, - \`jsonb\` json, + \`json\` blob, \`created_at\` timestamp not null default now() ) `, @@ -287,6 +294,14 @@ export function tests(driver?: string) { ) `, ); + + await db.execute(sql` + create table ${bigIntExample} ( + id serial primary key, + name text not null, + big_int blob not null + ) + `); }); async function setupReturningFunctionsTest(db: SingleStoreDatabase) { @@ -482,7 +497,7 @@ export function tests(driver?: string) { expect(result[0]!.createdAt).toBeInstanceOf(Date); // not timezone based timestamp, thats why it should not work here // t.assert(Math.abs(result[0]!.createdAt.getTime() - now) < 2000); - expect(result).toEqual([{ id: 1, name: 'John', verified: false, jsonb: null, createdAt: result[0]!.createdAt }]); + expect(result).toEqual([{ id: 1, name: 'John', verified: false, json: null, createdAt: result[0]!.createdAt }]); }); test('select sql', async (ctx) => { @@ -603,7 +618,7 @@ export function tests(driver?: string) { expect(users[0]!.createdAt).toBeInstanceOf(Date); // not timezone based timestamp, thats why it should not work here // t.assert(Math.abs(users[0]!.createdAt.getTime() - now) < 2000); - expect(users).toEqual([{ id: 1, name: 'Jane', verified: false, jsonb: null, createdAt: users[0]!.createdAt }]); + expect(users).toEqual([{ id: 1, name: 'Jane', verified: false, json: null, createdAt: users[0]!.createdAt }]); }); test('update with returning partial', async (ctx) => { @@ -644,27 +659,27 @@ export function tests(driver?: string) { await db.insert(usersTable).values({ id: 1, name: 'John' }); const result = await db.select().from(usersTable); - expect(result).toEqual([{ id: 1, name: 'John', verified: false, jsonb: null, createdAt: result[0]!.createdAt }]); + expect(result).toEqual([{ id: 1, name: 'John', verified: false, json: null, createdAt: result[0]!.createdAt }]); await db.insert(usersTable).values({ id: 2, name: 'Jane' }); const result2 = await db.select().from(usersTable).orderBy(asc(usersTable.id)); expect(result2).toEqual([ - { id: 1, name: 'John', verified: false, jsonb: null, createdAt: result2[0]!.createdAt }, - { id: 2, name: 'Jane', verified: false, jsonb: null, createdAt: result2[1]!.createdAt }, + { id: 1, name: 'John', verified: false, json: null, createdAt: result2[0]!.createdAt }, + { id: 2, name: 'Jane', verified: false, json: null, createdAt: result2[1]!.createdAt }, ]); }); test('json insert', async (ctx) => { const { db } = ctx.singlestore; - await db.insert(usersTable).values({ id: 1, name: 'John', jsonb: ['foo', 'bar'] }); + await db.insert(usersTable).values({ id: 1, name: 'John', json: ['foo', 'bar'] }); const result = await db.select({ id: usersTable.id, name: usersTable.name, - jsonb: usersTable.jsonb, + json: usersTable.json, }).from(usersTable); - expect(result).toEqual([{ id: 1, name: 'John', jsonb: ['foo', 'bar'] }]); + expect(result).toEqual([{ id: 1, name: 'John', json: ['foo', 'bar'] }]); }); test('insert with overridden default values', async (ctx) => { @@ -673,7 +688,7 @@ export function tests(driver?: string) { await db.insert(usersTable).values({ id: 1, name: 'John', verified: true }); const result = await db.select().from(usersTable); - expect(result).toEqual([{ id: 1, name: 'John', verified: true, jsonb: null, createdAt: result[0]!.createdAt }]); + expect(result).toEqual([{ id: 1, name: 'John', verified: true, json: null, createdAt: result[0]!.createdAt }]); }); test('insert many', async (ctx) => { @@ -681,23 +696,23 @@ export function tests(driver?: string) { await db.insert(usersTable).values([ { id: 1, name: 'John' }, - { id: 2, name: 'Bruce', jsonb: ['foo', 'bar'] }, + { id: 2, name: 'Bruce', json: ['foo', 'bar'] }, { id: 3, name: 'Jane' }, { id: 4, name: 'Austin', verified: true }, ]); const result = await db.select({ id: usersTable.id, name: usersTable.name, - jsonb: usersTable.jsonb, + json: usersTable.json, verified: usersTable.verified, }).from(usersTable) .orderBy(asc(usersTable.id)); expect(result).toEqual([ - { id: 1, name: 'John', jsonb: null, verified: false }, - { id: 2, name: 'Bruce', jsonb: ['foo', 'bar'], verified: false }, - { id: 3, name: 'Jane', jsonb: null, verified: false }, - { id: 4, name: 'Austin', jsonb: null, verified: true }, + { id: 1, name: 'John', json: null, verified: false }, + { id: 2, name: 'Bruce', json: ['foo', 'bar'], verified: false }, + { id: 3, name: 'Jane', json: null, verified: false }, + { id: 4, name: 'Austin', json: null, verified: true }, ]); }); @@ -706,7 +721,7 @@ export function tests(driver?: string) { const result = await db.insert(usersTable).values([ { name: 'John' }, - { name: 'Bruce', jsonb: ['foo', 'bar'] }, + { name: 'Bruce', json: ['foo', 'bar'] }, { name: 'Jane' }, { name: 'Austin', verified: true }, ]); @@ -951,13 +966,13 @@ export function tests(driver?: string) { const { db } = ctx.singlestore; const query = db.insert(usersTable) - .values({ id: 1, name: 'John', jsonb: ['foo', 'bar'] }) + .values({ id: 1, name: 'John', json: ['foo', 'bar'] }) .onDuplicateKeyUpdate({ set: { id: 1, name: 'John1' } }) .toSQL(); expect(query).toEqual({ sql: - 'insert into `userstest` (`id`, `name`, `verified`, `jsonb`, `created_at`) values (?, ?, default, ?, default) on duplicate key update `id` = ?, `name` = ?', + 'insert into `userstest` (`id`, `name`, `verified`, `json`, `created_at`) values (?, ?, default, ?, default) on duplicate key update `id` = ?, `name` = ?', params: [1, 'John', '["foo","bar"]', 1, 'John1'], }); }); @@ -3012,7 +3027,7 @@ export function tests(driver?: string) { expect(result[0]!.createdAt).toBeInstanceOf(Date); // not timezone based timestamp, thats why it should not work here // t.assert(Math.abs(result[0]!.createdAt.getTime() - now) < 2000); - expect(result).toEqual([{ id: 1, name: 'John', verified: false, jsonb: null, createdAt: result[0]!.createdAt }]); + expect(result).toEqual([{ id: 1, name: 'John', verified: false, json: null, createdAt: result[0]!.createdAt }]); }); test('mySchema :: select sql', async (ctx) => { @@ -3121,13 +3136,13 @@ export function tests(driver?: string) { await db.insert(usersMySchemaTable).values({ id: 1, name: 'John' }); const result = await db.select().from(usersMySchemaTable); - expect(result).toEqual([{ id: 1, name: 'John', verified: false, jsonb: null, createdAt: result[0]!.createdAt }]); + expect(result).toEqual([{ id: 1, name: 'John', verified: false, json: null, createdAt: result[0]!.createdAt }]); await db.insert(usersMySchemaTable).values({ id: 2, name: 'Jane' }); const result2 = await db.select().from(usersMySchemaTable).orderBy(asc(usersMySchemaTable.id)); expect(result2).toEqual([ - { id: 1, name: 'John', verified: false, jsonb: null, createdAt: result2[0]!.createdAt }, - { id: 2, name: 'Jane', verified: false, jsonb: null, createdAt: result2[1]!.createdAt }, + { id: 1, name: 'John', verified: false, json: null, createdAt: result2[0]!.createdAt }, + { id: 2, name: 'Jane', verified: false, json: null, createdAt: result2[1]!.createdAt }, ]); }); @@ -3138,7 +3153,7 @@ export function tests(driver?: string) { await db.insert(usersMySchemaTable).values({ id: 1, name: 'John', verified: true }); const result = await db.select().from(usersMySchemaTable); - expect(result).toEqual([{ id: 1, name: 'John', verified: true, jsonb: null, createdAt: result[0]!.createdAt }]); + expect(result).toEqual([{ id: 1, name: 'John', verified: true, json: null, createdAt: result[0]!.createdAt }]); }); test('mySchema :: insert many', async (ctx) => { @@ -3147,23 +3162,23 @@ export function tests(driver?: string) { await db.insert(usersMySchemaTable).values([ { id: 1, name: 'John' }, - { id: 2, name: 'Bruce', jsonb: ['foo', 'bar'] }, + { id: 2, name: 'Bruce', json: ['foo', 'bar'] }, { id: 3, name: 'Jane' }, { id: 4, name: 'Austin', verified: true }, ]); const result = await db.select({ id: usersMySchemaTable.id, name: usersMySchemaTable.name, - jsonb: usersMySchemaTable.jsonb, + json: usersMySchemaTable.json, verified: usersMySchemaTable.verified, }).from(usersMySchemaTable) .orderBy(asc(usersMySchemaTable.id)); expect(result).toEqual([ - { id: 1, name: 'John', jsonb: null, verified: false }, - { id: 2, name: 'Bruce', jsonb: ['foo', 'bar'], verified: false }, - { id: 3, name: 'Jane', jsonb: null, verified: false }, - { id: 4, name: 'Austin', jsonb: null, verified: true }, + { id: 1, name: 'John', json: null, verified: false }, + { id: 2, name: 'Bruce', json: ['foo', 'bar'], verified: false }, + { id: 3, name: 'Jane', json: null, verified: false }, + { id: 4, name: 'Austin', json: null, verified: true }, ]); }); @@ -3252,7 +3267,7 @@ export function tests(driver?: string) { \`id\` serial primary key, \`name\` text not null, \`verified\` boolean not null default false, - \`jsonb\` json, + \`json\` blob, \`created_at\` timestamp not null default now() ) `, @@ -3273,14 +3288,14 @@ export function tests(driver?: string) { id: 10, name: 'Ivan', verified: false, - jsonb: null, + json: null, createdAt: result[0]!.userstest.createdAt, }, customer: { id: 11, name: 'Hans', verified: false, - jsonb: null, + json: null, createdAt: result[0]!.customer!.createdAt, }, }]);