diff --git a/changelogs/drizzle-orm/0.17.7.md b/changelogs/drizzle-orm/0.17.7.md new file mode 100644 index 000000000..492140ee7 --- /dev/null +++ b/changelogs/drizzle-orm/0.17.7.md @@ -0,0 +1,4 @@ +- Fix [#158](https://github.com/drizzle-team/drizzle-orm/issues/158) issue. Method `.returning()` was working incorrectly with `.get()` method in sqlite dialect +- Fix SQLite Proxy driver mapping bug +- Add test cases for SQLite Proxy driver +- Add additional example for SQLite Proxy Server setup to handle `.get()` as well \ No newline at end of file diff --git a/drizzle-orm/package.json b/drizzle-orm/package.json index 6bdf7e3e0..9129d3185 100644 --- a/drizzle-orm/package.json +++ b/drizzle-orm/package.json @@ -1,6 +1,6 @@ { "name": "drizzle-orm", - "version": "0.17.6", + "version": "0.17.7", "description": "Drizzle ORM package for SQL databases", "scripts": { "build": "tsc && resolve-tspaths && cp ../README.md package.json dist/", diff --git a/drizzle-orm/src/better-sqlite3/session.ts b/drizzle-orm/src/better-sqlite3/session.ts index a149ee503..fcb422a8e 100644 --- a/drizzle-orm/src/better-sqlite3/session.ts +++ b/drizzle-orm/src/better-sqlite3/session.ts @@ -74,13 +74,14 @@ export class PreparedQuery get(placeholderValues?: Record): T['get'] { const params = fillPlaceholders(this.params, placeholderValues ?? {}); this.logger.logQuery(this.queryString, params); - const value = this.stmt.get(...params); const { fields } = this; if (!fields) { - return value; + return this.stmt.get(...params); } + const value = this.stmt.raw().get(...params); + return mapResultRow(fields, value); } diff --git a/drizzle-orm/src/sqlite-proxy/driver.ts b/drizzle-orm/src/sqlite-proxy/driver.ts index d1b42f63b..b1c852238 100644 --- a/drizzle-orm/src/sqlite-proxy/driver.ts +++ b/drizzle-orm/src/sqlite-proxy/driver.ts @@ -13,11 +13,13 @@ export interface SqliteRemoteResult { export type SqliteRemoteDatabase = BaseSQLiteDatabase<'async', SqliteRemoteResult>; -export type RemoteCallback = ( +export type AsyncRemoteCallback = ( sql: string, params: any[], - method: 'run' | 'all' | 'values', -) => Promise<{ rows: any[][] }>; + method: 'run' | 'all' | 'values' | 'get', +) => Promise<{ rows: any[] }>; + +export type RemoteCallback = AsyncRemoteCallback; export function drizzle(callback: RemoteCallback, config: DrizzleConfig = {}): SqliteRemoteDatabase { const dialect = new SQLiteAsyncDialect(); diff --git a/drizzle-orm/src/sqlite-proxy/session.ts b/drizzle-orm/src/sqlite-proxy/session.ts index 35164b50c..058aef19c 100644 --- a/drizzle-orm/src/sqlite-proxy/session.ts +++ b/drizzle-orm/src/sqlite-proxy/session.ts @@ -34,7 +34,10 @@ export class SQLiteRemoteSession extends SQLiteSession<'async', SqliteRemoteResu // return this.client(this.queryString, params).then(({ rows }) => rows!) } - prepareQuery>(query: Query, fields?: SelectFieldsOrdered): PreparedQuery { + prepareQuery>( + query: Query, + fields?: SelectFieldsOrdered, + ): PreparedQuery { return new PreparedQuery(this.client, query.sql, query.params, this.logger, fields); } } @@ -60,17 +63,35 @@ export class PreparedQuery async all(placeholderValues?: Record): Promise { const { fields } = this; - if (fields) { - return this.values(placeholderValues).then((values) => values.map((row) => mapResultRow(fields, row))); - } const params = fillPlaceholders(this.params, placeholderValues ?? {}); this.logger.logQuery(this.queryString, params); + + const clientResult = this.client(this.queryString, params, 'all'); + + if (fields) { + return clientResult.then((values) => values.rows.map((row) => mapResultRow(fields, row))); + } + return this.client(this.queryString, params, 'all').then(({ rows }) => rows!); } async get(placeholderValues?: Record): Promise { - return await this.all(placeholderValues).then((rows) => rows[0]); + const { fields } = this; + + const params = fillPlaceholders(this.params, placeholderValues ?? {}); + this.logger.logQuery(this.queryString, params); + + const clientResult = await this.client(this.queryString, params, 'get'); + + if (fields) { + if (typeof clientResult.rows === 'undefined') { + return mapResultRow(fields, []); + } + return mapResultRow(fields, clientResult.rows); + } + + return clientResult.rows; } async values(placeholderValues?: Record): Promise { diff --git a/examples/sqlite-proxy/README.md b/examples/sqlite-proxy/README.md index e582d6dbc..e07f96084 100644 --- a/examples/sqlite-proxy/README.md +++ b/examples/sqlite-proxy/README.md @@ -9,7 +9,7 @@ Subscribe to our updates on [Twitter](https://twitter.com/DrizzleOrm) and [Disco SQLite Proxy driver will do all the work except of 2 things, that you will be responsible for: 1. Calls to database, http servers or any other way to communicate with database -2. Mapping data from database to `{rows: string[][], ...additional db response params}` format. Only `row` field is required +2. Mapping data from database to `{rows: any[], ...additional db response params}` format. Only `rows` field is required. Rows should be a row array from database
This project has simple example of defining http proxy server, that will proxy all calls from drizzle orm to database and back. This example could perfectly fit for serverless applications @@ -32,6 +32,8 @@ This project has simple example of defining http proxy server, that will proxy a > **Warning**: > You will be responsible for proper error handling in this part. Drizzle always waits for `{rows: string[][]}` so if any error was on http call(or any other call) - be sure, that you return at least empty array back +> +> For `get` method you should return `{rows: string[]}`
@@ -59,7 +61,7 @@ We have 3 params, that will be sent to server. It's your decision which of them 1. `sql` - SQL query (`SELECT * FROM users WHERE id = ?`) 2. `params` - params, that should be sent on database call (For query above it could be: `[1]`) -3. `method` - Method, that was executed (`run` | `all` | `values`). Hint for proxy server on which sqlite method to invoke +3. `method` - Method, that was executed (`run` | `all` | `values` | `get`). Hint for proxy server on which sqlite method to invoke ### Migrations using SQLite Proxy @@ -78,15 +80,14 @@ In current SQLite Proxy version - drizzle don't handle transactions for migratio import axios from 'axios'; import { migrate } from 'drizzle-orm/sqlite-proxy/migrator'; - await migrate(db, async (queries) => { - try { - await axios.post('http://localhost:3000/migrate', { queries }); - } catch (e) { - console.log(e) - throw Error('Proxy server cannot run migrations') - } - }, { migrationsFolder: 'drizzle' }); + try { + await axios.post('http://localhost:3000/migrate', { queries }); + } catch (e) { + console.log(e); + throw Error('Proxy server cannot run migrations'); + } +}, { migrationsFolder: 'drizzle' }); ``` 1. `queries` - array of sql statements, that should be run on migration @@ -116,14 +117,21 @@ app.post('/query', (req, res) => { const result = db.prepare(sqlBody).run(params); res.send(result); } catch (e: any) { - res.status(500).json({ error: e.message }); + res.status(500).json({ error: e.message }); } } else if (method === 'all' || method === 'values') { - try { + try { const rows = db.prepare(sqlBody).raw().all(params); - res.send(rows); + res.send(rows); + } catch (e: any) { + res.status(500).json({ error: e.message }); + } + } else if (method === 'get') { + try { + const row = this.db.prepare(sql).raw().get(params); + return { data: row }; } catch (e: any) { - res.status(500).json({ error: e.message }); + return { error: e.message }; } } else { res.status(500).json({ error: 'Unkown method value' }); diff --git a/examples/sqlite-proxy/drizzle/20230202162455/migration.sql b/examples/sqlite-proxy/drizzle/20230202162455/migration.sql deleted file mode 100644 index 3dc931590..000000000 --- a/examples/sqlite-proxy/drizzle/20230202162455/migration.sql +++ /dev/null @@ -1,5 +0,0 @@ -CREATE TABLE users12 ( - `id` integer PRIMARY KEY NOT NULL, - `name` text NOT NULL, - `email` text NOT NULL -); diff --git a/examples/sqlite-proxy/drizzle/20230202162455/snapshot.json b/examples/sqlite-proxy/drizzle/20230202162455/snapshot.json deleted file mode 100644 index fb50d5209..000000000 --- a/examples/sqlite-proxy/drizzle/20230202162455/snapshot.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "version": "4", - "dialect": "sqlite", - "id": "88e641f7-006e-45f2-8d76-d2477c5cc084", - "prevId": "00000000-0000-0000-0000-000000000000", - "tables": { - "users12": { - "name": "users12", - "columns": { - "id": { - "name": "id", - "type": "integer", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "email": { - "name": "email", - "type": "text", - "primaryKey": false, - "notNull": true - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {} - } - }, - "enums": {} -} \ No newline at end of file diff --git a/examples/sqlite-proxy/drizzle/20230205140048/migration.sql b/examples/sqlite-proxy/drizzle/20230205140048/migration.sql new file mode 100644 index 000000000..459b5ff92 --- /dev/null +++ b/examples/sqlite-proxy/drizzle/20230205140048/migration.sql @@ -0,0 +1,11 @@ +CREATE TABLE cities ( + `id` integer PRIMARY KEY NOT NULL, + `name` text NOT NULL +); + +CREATE TABLE users ( + `id` integer PRIMARY KEY NOT NULL, + `name` text NOT NULL, + `email` text NOT NULL, + `city_id` integer +); diff --git a/examples/sqlite-proxy/drizzle/20230205140048/snapshot.json b/examples/sqlite-proxy/drizzle/20230205140048/snapshot.json new file mode 100644 index 000000000..0fb47c1fd --- /dev/null +++ b/examples/sqlite-proxy/drizzle/20230205140048/snapshot.json @@ -0,0 +1,64 @@ +{ + "version": "4", + "dialect": "sqlite", + "id": "71c97670-a6f8-42e0-9a15-4991f89e2134", + "prevId": "00000000-0000-0000-0000-000000000000", + "tables": { + "cities": { + "name": "cities", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "city_id": { + "name": "city_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {} + } + }, + "enums": {} +} \ No newline at end of file diff --git a/examples/sqlite-proxy/src/index.ts b/examples/sqlite-proxy/src/index.ts index 8e3d96f2a..5f8095983 100644 --- a/examples/sqlite-proxy/src/index.ts +++ b/examples/sqlite-proxy/src/index.ts @@ -1,7 +1,8 @@ import axios from 'axios'; +import { eq } from 'drizzle-orm/expressions'; import { drizzle } from 'drizzle-orm/sqlite-proxy'; import { migrate } from 'drizzle-orm/sqlite-proxy/migrator'; -import { users } from './schema'; +import { cities, users } from './schema'; async function main() { const db = drizzle(async (sql, params, method) => { @@ -10,7 +11,7 @@ async function main() { return { rows: rows.data }; } catch (e: any) { - console.error('Error from sqlite proxy server: ', e.response.data) + console.error('Error from sqlite proxy server: ', e.response.data); return { rows: [] }; } }); @@ -19,16 +20,19 @@ async function main() { try { await axios.post('http://localhost:3000/migrate', { queries }); } catch (e) { - console.log(e) - throw Error('Proxy server cannot run migrations') + console.log(e); + throw Error('Proxy server cannot run migrations'); } }, { migrationsFolder: 'drizzle' }); - const insertResult = await db.insert(users).values({ id: 1, name: 'name', email: 'email' }).run(); - console.log(insertResult) + const insertedCity = await db.insert(cities).values({ id: 1, name: 'name' }).returning().get(); + console.log('insertedCity: ', insertedCity); - const usersResponse = await db.select(users).all(); - console.log(usersResponse); + const insertedUser = await db.insert(users).values({ id: 1, name: 'name', email: 'email', cityId: 1 }).run(); + console.log('insertedUser: ', insertedUser); + + const usersToCityResponse = await db.select(users).leftJoin(cities, eq(users.cityId, cities.id)).get(); + console.log('usersToCityResponse: ', usersToCityResponse); } -main(); \ No newline at end of file +main(); diff --git a/examples/sqlite-proxy/src/schema.ts b/examples/sqlite-proxy/src/schema.ts index 8e560252f..4d9f21a5f 100644 --- a/examples/sqlite-proxy/src/schema.ts +++ b/examples/sqlite-proxy/src/schema.ts @@ -1,7 +1,13 @@ import { int, sqliteTable, text } from "drizzle-orm/sqlite-core"; -export const users = sqliteTable('users12', { +export const users = sqliteTable('users', { id: int('id').primaryKey(), name: text('name').notNull(), email: text('email').notNull(), + cityId: int('city_id'), +}); + +export const cities = sqliteTable('cities', { + id: int('id').primaryKey(), + name: text('name').notNull(), }); diff --git a/examples/sqlite-proxy/src/server.ts b/examples/sqlite-proxy/src/server.ts index f5264a962..ecf1c9613 100644 --- a/examples/sqlite-proxy/src/server.ts +++ b/examples/sqlite-proxy/src/server.ts @@ -24,6 +24,13 @@ app.post('/query', (req, res) => { } catch (e: any) { res.status(500).json({ error: e.message }); } + }else if (method === 'get') { + try { + const row = db.prepare(sqlBody).raw().get(params); + return { data: row }; + } catch (e: any) { + return { error: e.message }; + } } else { res.status(500).json({ error: 'Unkown method value' }); } diff --git a/integration-tests/tests/better-sqlite.test.ts b/integration-tests/tests/better-sqlite.test.ts index 237c104e5..dc4fc9dd5 100644 --- a/integration-tests/tests/better-sqlite.test.ts +++ b/integration-tests/tests/better-sqlite.test.ts @@ -117,6 +117,16 @@ test.serial('insert returning sql', (t) => { t.deepEqual(users, [{ name: 'JOHN' }]); }); +test.serial('insert returning sql + get()', (t) => { + const { db } = t.context; + + const users = db.insert(usersTable).values({ name: 'John' }).returning({ + name: sql`upper(${usersTable.name})`, + }).get(); + + t.deepEqual(users, { name: 'JOHN' }); +}); + test.serial('delete returning sql', (t) => { const { db } = t.context; @@ -139,6 +149,17 @@ test.serial('update returning sql', (t) => { t.deepEqual(users, [{ name: 'JANE' }]); }); +test.serial('update returning sql + get()', (t) => { + const { db } = t.context; + + db.insert(usersTable).values({ name: 'John' }).run(); + const users = db.update(usersTable).set({ name: 'Jane' }).where(eq(usersTable.name, 'John')).returning({ + name: sql`upper(${usersTable.name})`, + }).get(); + + t.deepEqual(users, { name: 'JANE' }); +}); + test.serial('insert with auto increment', (t) => { const { db } = t.context; @@ -189,6 +210,19 @@ test.serial('update with returning all fields', (t) => { t.deepEqual(users, [{ id: 1, name: 'Jane', verified: 0, json: null, createdAt: users[0]!.createdAt }]); }); +test.serial('update with returning all fields + get()', (t) => { + const { db } = t.context; + + const now = Date.now(); + + db.insert(usersTable).values({ name: 'John' }).run(); + const users = db.update(usersTable).set({ name: 'Jane' }).where(eq(usersTable.name, 'John')).returning().get(); + + t.assert(users.createdAt instanceof Date); + t.assert(Math.abs(users.createdAt.getTime() - now) < 100); + t.deepEqual(users, { id: 1, name: 'Jane', verified: 0, json: null, createdAt: users.createdAt }); +}); + test.serial('update with returning partial', (t) => { const { db } = t.context; @@ -214,6 +248,19 @@ test.serial('delete with returning all fields', (t) => { t.deepEqual(users, [{ id: 1, name: 'John', verified: 0, json: null, createdAt: users[0]!.createdAt }]); }); +test.serial('delete with returning all fields + get()', (t) => { + const { db } = t.context; + + const now = Date.now(); + + db.insert(usersTable).values({ name: 'John' }).run(); + const users = db.delete(usersTable).where(eq(usersTable.name, 'John')).returning().get(); + + t.assert(users!.createdAt instanceof Date); + t.assert(Math.abs(users!.createdAt.getTime() - now) < 100); + t.deepEqual(users, { id: 1, name: 'John', verified: 0, json: null, createdAt: users!.createdAt }); +}); + test.serial('delete with returning partial', (t) => { const { db } = t.context; @@ -226,6 +273,18 @@ test.serial('delete with returning partial', (t) => { t.deepEqual(users, [{ id: 1, name: 'John' }]); }); +test.serial('delete with returning partial + get()', (t) => { + const { db } = t.context; + + db.insert(usersTable).values({ name: 'John' }).run(); + const users = db.delete(usersTable).where(eq(usersTable.name, 'John')).returning({ + id: usersTable.id, + name: usersTable.name, + }).get(); + + t.deepEqual(users, { id: 1, name: 'John' }); +}); + test.serial('insert + select', (t) => { const { db } = t.context; diff --git a/integration-tests/tests/sqlite-proxy.test.ts b/integration-tests/tests/sqlite-proxy.test.ts new file mode 100644 index 000000000..7f47adb0f --- /dev/null +++ b/integration-tests/tests/sqlite-proxy.test.ts @@ -0,0 +1,691 @@ +import anyTest, { TestFn } from 'ava'; +import BetterSqlite3 from 'better-sqlite3'; +import Database from 'better-sqlite3'; +import { sql } from 'drizzle-orm'; +import { BetterSQLite3Database } from 'drizzle-orm/better-sqlite3'; +import { asc, eq } from 'drizzle-orm/expressions'; +import { name, placeholder } from 'drizzle-orm/sql'; +import { alias, blob, InferModel, integer, primaryKey, sqliteTable, text } from 'drizzle-orm/sqlite-core'; +import { drizzle as proxyDrizzle, SqliteRemoteDatabase } from 'drizzle-orm/sqlite-proxy'; +import { migrate } from 'drizzle-orm/sqlite-proxy/migrator'; + +class ServerSimulator { + constructor(private db: BetterSqlite3.Database) { + } + + async query(sql: string, params: any[], method: string) { + if (method === 'run') { + try { + const result = this.db.prepare(sql).run(params); + return { data: result }; + } catch (e: any) { + return { error: e.message }; + } + } else if (method === 'all' || method === 'values') { + try { + const rows = this.db.prepare(sql).raw().all(params); + return { data: rows }; + } catch (e: any) { + return { error: e.message }; + } + } else if (method === 'get') { + try { + const row = this.db.prepare(sql).raw().get(params); + return { data: row }; + } catch (e: any) { + console.log('get row: ', e); + return { error: e.message }; + } + } else { + return { error: 'Unkown method value' }; + } + } + + migrations(queries: string[]) { + this.db.exec('BEGIN'); + try { + for (const query of queries) { + this.db.exec(query); + } + this.db.exec('COMMIT'); + } catch (e: any) { + this.db.exec('ROLLBACK'); + } + + return {}; + } +} + +const usersTable = sqliteTable('users', { + id: integer('id').primaryKey(), + name: text('name').notNull(), + verified: integer('verified').notNull().default(0), + json: blob('json', { mode: 'json' }), + createdAt: integer('created_at', { mode: 'timestamp' }).notNull().defaultNow(), +}); + +const usersMigratorTable = sqliteTable('users12', { + id: integer('id').primaryKey(), + name: text('name').notNull(), + email: text('email').notNull(), +}); + +const anotherUsersMigratorTable = sqliteTable('another_users', { + id: integer('id').primaryKey(), + name: text('name').notNull(), + email: text('email').notNull(), +}); + +const pkExample = sqliteTable('pk_example', { + id: integer('id').primaryKey(), + name: text('name').notNull(), + email: text('email').notNull(), +}, (table) => ({ + compositePk: primaryKey(table.id, table.name), +})); + +interface Context { + db: SqliteRemoteDatabase; + client: Database.Database; + serverSimulator: ServerSimulator; +} + +const test = anyTest as TestFn; + +test.before((t) => { + const ctx = t.context; + const dbPath = process.env['SQLITE_DB_PATH'] ?? ':memory:'; + + ctx.client = new Database(dbPath); + + ctx.serverSimulator = new ServerSimulator(ctx.client); + + ctx.db = proxyDrizzle(async (sql, params, method) => { + try { + const rows = await ctx.serverSimulator.query(sql, params, method); + + if (typeof rows.error !== 'undefined') { + throw Error(rows.error); + } + + return { rows: rows.data }; + } catch (e: any) { + console.error('Error from sqlite proxy server: ', e.response.data); + return { rows: [] }; + } + }); +}); + +test.beforeEach(async (t) => { + const ctx = t.context; + ctx.db.run(sql`drop table if exists ${usersTable}`); + ctx.db.run(sql` + create table ${usersTable} ( + id integer primary key, + name text not null, + verified integer not null default 0, + json blob, + created_at integer not null default (cast((julianday('now') - 2440587.5)*86400000 as integer)) + )`); +}); + +test.serial('select all fields', async (t) => { + const { db } = t.context; + + const now = Date.now(); + + await db.insert(usersTable).values({ name: 'John' }).run(); + const result = await db.select(usersTable).all(); + + t.assert(result[0]!.createdAt instanceof Date); + t.assert(Math.abs(result[0]!.createdAt.getTime() - now) < 100); + t.deepEqual(result, [{ id: 1, name: 'John', verified: 0, json: null, createdAt: result[0]!.createdAt }]); +}); + +test.serial('select partial', async (t) => { + const { db } = t.context; + + await db.insert(usersTable).values({ name: 'John' }).run(); + const result = await db.select(usersTable).fields({ name: usersTable.name }).all(); + + t.deepEqual(result, [{ name: 'John' }]); +}); + +test.serial('select sql', async (t) => { + const { db } = t.context; + + await db.insert(usersTable).values({ name: 'John' }).run(); + const users = await db.select(usersTable).fields({ + name: sql`upper(${usersTable.name})`, + }).all(); + + t.deepEqual(users, [{ name: 'JOHN' }]); +}); + +test.serial('select typed sql', async (t) => { + const { db } = t.context; + + await db.insert(usersTable).values({ name: 'John' }).run(); + const users = await db.select(usersTable).fields({ + name: sql`upper(${usersTable.name})`.as(), + }).all(); + + t.deepEqual(users, [{ name: 'JOHN' }]); +}); + +test.serial('insert returning sql', async (t) => { + const { db } = t.context; + + const users = await db.insert(usersTable).values({ name: 'John' }).returning({ + name: sql`upper(${usersTable.name})`, + }).all(); + + t.deepEqual(users, [{ name: 'JOHN' }]); +}); + +test.serial('insert returning sql + get()', async (t) => { + const { db } = t.context; + + const users = await db.insert(usersTable).values({ name: 'John' }).returning({ + name: sql`upper(${usersTable.name})`, + }).get(); + + t.deepEqual(users, { name: 'JOHN' }); +}); + +test.serial('delete returning sql', async (t) => { + const { db } = t.context; + + await db.insert(usersTable).values({ name: 'John' }).run(); + const users = await db.delete(usersTable).where(eq(usersTable.name, 'John')).returning({ + name: sql`upper(${usersTable.name})`, + }).all(); + + t.deepEqual(users, [{ name: 'JOHN' }]); +}); + +test.serial('update returning sql', async (t) => { + const { db } = t.context; + + await db.insert(usersTable).values({ name: 'John' }).run(); + const users = await db.update(usersTable).set({ name: 'Jane' }).where(eq(usersTable.name, 'John')).returning({ + name: sql`upper(${usersTable.name})`, + }).all(); + + t.deepEqual(users, [{ name: 'JANE' }]); +}); + +test.serial('update returning sql + get()', async (t) => { + const { db } = t.context; + + await db.insert(usersTable).values({ name: 'John' }).run(); + const users = await db.update(usersTable).set({ name: 'Jane' }).where(eq(usersTable.name, 'John')).returning({ + name: sql`upper(${usersTable.name})`, + }).get(); + + t.deepEqual(users, { name: 'JANE' }); +}); + +test.serial('insert with auto increment', async (t) => { + const { db } = t.context; + + await db.insert(usersTable).values( + { name: 'John' }, + { name: 'Jane' }, + { name: 'George' }, + { name: 'Austin' }, + ).run(); + const result = await db.select(usersTable).fields({ id: usersTable.id, name: usersTable.name }).all(); + + t.deepEqual(result, [ + { id: 1, name: 'John' }, + { id: 2, name: 'Jane' }, + { id: 3, name: 'George' }, + { id: 4, name: 'Austin' }, + ]); +}); + +test.serial('insert with default values', async (t) => { + const { db } = t.context; + + await db.insert(usersTable).values({ name: 'John' }).run(); + const result = await db.select(usersTable).all(); + + t.deepEqual(result, [{ id: 1, name: 'John', verified: 0, json: null, createdAt: result[0]!.createdAt }]); +}); + +test.serial('insert with overridden default values', async (t) => { + const { db } = t.context; + + await db.insert(usersTable).values({ name: 'John', verified: 1 }).run(); + const result = await db.select(usersTable).all(); + + t.deepEqual(result, [{ id: 1, name: 'John', verified: 1, json: null, createdAt: result[0]!.createdAt }]); +}); + +test.serial('update with returning all fields', async (t) => { + const { db } = t.context; + + const now = Date.now(); + + await db.insert(usersTable).values({ name: 'John' }).run(); + const users = await db.update(usersTable).set({ name: 'Jane' }).where(eq(usersTable.name, 'John')).returning().all(); + + t.assert(users[0]!.createdAt instanceof Date); + t.assert(Math.abs(users[0]!.createdAt.getTime() - now) < 100); + t.deepEqual(users, [{ id: 1, name: 'Jane', verified: 0, json: null, createdAt: users[0]!.createdAt }]); +}); + +test.serial('update with returning all fields + get()', async (t) => { + const { db } = t.context; + + const now = Date.now(); + + await db.insert(usersTable).values({ name: 'John' }).run(); + const users = await db.update(usersTable).set({ name: 'Jane' }).where(eq(usersTable.name, 'John')).returning().get(); + + t.assert(users.createdAt instanceof Date); + t.assert(Math.abs(users.createdAt.getTime() - now) < 100); + t.deepEqual(users, { id: 1, name: 'Jane', verified: 0, json: null, createdAt: users.createdAt }); +}); + +test.serial('update with returning partial', async (t) => { + const { db } = t.context; + + await db.insert(usersTable).values({ name: 'John' }).run(); + const users = await db.update(usersTable).set({ name: 'Jane' }).where(eq(usersTable.name, 'John')).returning({ + id: usersTable.id, + name: usersTable.name, + }).all(); + + t.deepEqual(users, [{ id: 1, name: 'Jane' }]); +}); + +test.serial('delete with returning all fields', async (t) => { + const { db } = t.context; + + const now = Date.now(); + + await db.insert(usersTable).values({ name: 'John' }).run(); + const users = await db.delete(usersTable).where(eq(usersTable.name, 'John')).returning().all(); + + t.assert(users[0]!.createdAt instanceof Date); + t.assert(Math.abs(users[0]!.createdAt.getTime() - now) < 100); + t.deepEqual(users, [{ id: 1, name: 'John', verified: 0, json: null, createdAt: users[0]!.createdAt }]); +}); + +test.serial('delete with returning all fields + get()', async (t) => { + const { db } = t.context; + + const now = Date.now(); + + await db.insert(usersTable).values({ name: 'John' }).run(); + const users = await db.delete(usersTable).where(eq(usersTable.name, 'John')).returning().get(); + + t.assert(users!.createdAt instanceof Date); + t.assert(Math.abs(users!.createdAt.getTime() - now) < 100); + t.deepEqual(users, { id: 1, name: 'John', verified: 0, json: null, createdAt: users!.createdAt }); +}); + +test.serial('delete with returning partial', async (t) => { + const { db } = t.context; + + await db.insert(usersTable).values({ name: 'John' }).run(); + const users = await db.delete(usersTable).where(eq(usersTable.name, 'John')).returning({ + id: usersTable.id, + name: usersTable.name, + }).all(); + + t.deepEqual(users, [{ id: 1, name: 'John' }]); +}); + +test.serial('delete with returning partial + get()', async (t) => { + const { db } = t.context; + + await db.insert(usersTable).values({ name: 'John' }).run(); + const users = await db.delete(usersTable).where(eq(usersTable.name, 'John')).returning({ + id: usersTable.id, + name: usersTable.name, + }).get(); + + t.deepEqual(users, { id: 1, name: 'John' }); +}); + +test.serial('insert + select', async (t) => { + const { db } = t.context; + + await db.insert(usersTable).values({ name: 'John' }).run(); + const result = await db.select(usersTable).fields({ id: usersTable.id, name: usersTable.name }).all(); + + t.deepEqual(result, [{ id: 1, name: 'John' }]); + + await db.insert(usersTable).values({ name: 'Jane' }).run(); + const result2 = await db.select(usersTable).fields({ id: usersTable.id, name: usersTable.name }).all(); + + t.deepEqual(result2, [{ id: 1, name: 'John' }, { id: 2, name: 'Jane' }]); +}); + +test.serial('json insert', async (t) => { + const { db } = t.context; + + await db.insert(usersTable).values({ name: 'John', json: ['foo', 'bar'] }).run(); + const result = await db.select(usersTable).fields({ + id: usersTable.id, + name: usersTable.name, + json: usersTable.json, + }).all(); + + t.deepEqual(result, [{ id: 1, name: 'John', json: ['foo', 'bar'] }]); +}); + +test.serial('insert many', async (t) => { + const { db } = t.context; + + await db.insert(usersTable).values( + { name: 'John' }, + { name: 'Bruce', json: ['foo', 'bar'] }, + { name: 'Jane' }, + { name: 'Austin', verified: 1 }, + ).run(); + const result = await db.select(usersTable).fields({ + id: usersTable.id, + name: usersTable.name, + json: usersTable.json, + verified: usersTable.verified, + }).all(); + + t.deepEqual(result, [ + { id: 1, name: 'John', json: null, verified: 0 }, + { id: 2, name: 'Bruce', json: ['foo', 'bar'], verified: 0 }, + { id: 3, name: 'Jane', json: null, verified: 0 }, + { id: 4, name: 'Austin', json: null, verified: 1 }, + ]); +}); + +test.serial('insert many with returning', async (t) => { + const { db } = t.context; + + const result = await db.insert(usersTable).values( + { name: 'John' }, + { name: 'Bruce', json: ['foo', 'bar'] }, + { name: 'Jane' }, + { name: 'Austin', verified: 1 }, + ) + .returning({ + id: usersTable.id, + name: usersTable.name, + json: usersTable.json, + verified: usersTable.verified, + }) + .all(); + + t.deepEqual(result, [ + { id: 1, name: 'John', json: null, verified: 0 }, + { id: 2, name: 'Bruce', json: ['foo', 'bar'], verified: 0 }, + { id: 3, name: 'Jane', json: null, verified: 0 }, + { id: 4, name: 'Austin', json: null, verified: 1 }, + ]); +}); + +test.serial('partial join with alias', async (t) => { + const { db } = t.context; + const customerAlias = alias(usersTable, 'customer'); + + await db.insert(usersTable).values({ id: 10, name: 'Ivan' }, { id: 11, name: 'Hans' }).run(); + const result = await db + .select(usersTable) + .fields({ + user: { + id: usersTable.id, + name: usersTable.name, + }, + customer: { + id: customerAlias.id, + name: customerAlias.name, + }, + }) + .leftJoin(customerAlias, eq(customerAlias.id, 11)) + .where(eq(usersTable.id, 10)) + .all(); + + t.deepEqual(result, [{ + user: { id: 10, name: 'Ivan' }, + customer: { id: 11, name: 'Hans' }, + }]); +}); + +test.serial('full join with alias', async (t) => { + const { db } = t.context; + const customerAlias = alias(usersTable, 'customer'); + + await db.insert(usersTable).values({ id: 10, name: 'Ivan' }, { id: 11, name: 'Hans' }).run(); + const result = await db + .select(usersTable) + .leftJoin(customerAlias, eq(customerAlias.id, 11)) + .where(eq(usersTable.id, 10)) + .all(); + + t.deepEqual(result, [{ + users: { + id: 10, + name: 'Ivan', + verified: 0, + json: null, + createdAt: result[0]!.users.createdAt, + }, + customer: { + id: 11, + name: 'Hans', + verified: 0, + json: null, + createdAt: result[0]!.customer!.createdAt, + }, + }]); +}); + +test.serial('insert with spaces', async (t) => { + const { db } = t.context; + + await db.insert(usersTable).values({ name: sql`'Jo h n'` }).run(); + const result = await db.select(usersTable).fields({ id: usersTable.id, name: usersTable.name }).all(); + + t.deepEqual(result, [{ id: 1, name: 'Jo h n' }]); +}); + +test.serial('prepared statement', async (t) => { + const { db } = t.context; + + await db.insert(usersTable).values({ name: 'John' }).run(); + const statement = db.select(usersTable).fields({ id: usersTable.id, name: usersTable.name }).prepare(); + const result = await statement.all(); + + t.deepEqual(result, [{ id: 1, name: 'John' }]); +}); + +test.serial('prepared statement reuse', async (t) => { + const { db } = t.context; + + const stmt = db.insert(usersTable).values({ + verified: 1, + name: placeholder('name'), + }).prepare(); + + for (let i = 0; i < 10; i++) { + await stmt.run({ name: `John ${i}` }); + } + + const result = await db.select(usersTable).fields({ + id: usersTable.id, + name: usersTable.name, + verified: usersTable.verified, + }).all(); + + t.deepEqual(result, [ + { id: 1, name: 'John 0', verified: 1 }, + { id: 2, name: 'John 1', verified: 1 }, + { id: 3, name: 'John 2', verified: 1 }, + { id: 4, name: 'John 3', verified: 1 }, + { id: 5, name: 'John 4', verified: 1 }, + { id: 6, name: 'John 5', verified: 1 }, + { id: 7, name: 'John 6', verified: 1 }, + { id: 8, name: 'John 7', verified: 1 }, + { id: 9, name: 'John 8', verified: 1 }, + { id: 10, name: 'John 9', verified: 1 }, + ]); +}); + +test.serial('prepared statement with placeholder in .where', async (t) => { + const { db } = t.context; + + await db.insert(usersTable).values({ name: 'John' }).run(); + const stmt = db.select(usersTable) + .fields({ + id: usersTable.id, + name: usersTable.name, + }) + .where(eq(usersTable.id, placeholder('id'))) + .prepare(); + const result = await stmt.all({ id: 1 }); + + t.deepEqual(result, [{ id: 1, name: 'John' }]); +}); + +test.serial('select with group by as field', async (t) => { + const { db } = t.context; + + await db.insert(usersTable).values({ name: 'John' }, { name: 'Jane' }, { name: 'Jane' }).run(); + + const result = await db.select(usersTable) + .fields({ name: usersTable.name }) + .groupBy(usersTable.name) + .all(); + + t.deepEqual(result, [{ name: 'Jane' }, { name: 'John' }]); +}); + +test.serial('select with group by as sql', async (t) => { + const { db } = t.context; + + await db.insert(usersTable).values({ name: 'John' }, { name: 'Jane' }, { name: 'Jane' }).run(); + + const result = await db.select(usersTable) + .fields({ name: usersTable.name }) + .groupBy(sql`${usersTable.name}`) + .all(); + + t.deepEqual(result, [{ name: 'Jane' }, { name: 'John' }]); +}); + +test.serial('select with group by as sql + column', async (t) => { + const { db } = t.context; + + await db.insert(usersTable).values({ name: 'John' }, { name: 'Jane' }, { name: 'Jane' }).run(); + + const result = await db.select(usersTable) + .fields({ name: usersTable.name }) + .groupBy(sql`${usersTable.name}`, usersTable.id) + .all(); + + t.deepEqual(result, [{ name: 'John' }, { name: 'Jane' }, { name: 'Jane' }]); +}); + +test.serial('select with group by as column + sql', async (t) => { + const { db } = t.context; + + await db.insert(usersTable).values({ name: 'John' }, { name: 'Jane' }, { name: 'Jane' }).run(); + + const result = await db.select(usersTable) + .fields({ name: usersTable.name }) + .groupBy(usersTable.id, sql`${usersTable.name}`) + .all(); + + t.deepEqual(result, [{ name: 'John' }, { name: 'Jane' }, { name: 'Jane' }]); +}); + +test.serial('select with group by complex query', async (t) => { + const { db } = t.context; + + await db.insert(usersTable).values({ name: 'John' }, { name: 'Jane' }, { name: 'Jane' }).run(); + + const result = await db.select(usersTable) + .fields({ name: usersTable.name }) + .groupBy(usersTable.id, sql`${usersTable.name}`) + .orderBy(asc(usersTable.name)) + .limit(1) + .all(); + + t.deepEqual(result, [{ name: 'Jane' }]); +}); + +test.serial('build query', async (t) => { + const { db } = t.context; + + const query = db.select(usersTable) + .fields({ id: usersTable.id, name: usersTable.name }) + .groupBy(usersTable.id, usersTable.name) + .toSQL(); + + t.deepEqual(query, { + sql: 'select "id", "name" from "users" group by "users"."id", "users"."name"', + params: [], + }); +}); + +// test.serial('migrator', async (t) => { +// const { db } = t.context; +// migrate(db, { migrationsFolder: './drizzle/sqlite' }); + +// db.insert(usersMigratorTable).values({ name: 'John', email: 'email' }).run(); +// const result = db.select(usersMigratorTable).all(); + +// db.insert(anotherUsersMigratorTable).values({ name: 'John', email: 'email' }).run(); +// const result2 = db.select(usersMigratorTable).all(); + +// t.deepEqual(result, [{ id: 1, name: 'John', email: 'email' }]); +// t.deepEqual(result2, [{ id: 1, name: 'John', email: 'email' }]); +// }); + +test.serial('insert via db.run + select via db.all', async (t) => { + const { db } = t.context; + + await db.run(sql`insert into ${usersTable} (${name(usersTable.name.name)}) values (${'John'})`); + + const result = await db.all(sql`select id, name from "users"`); + t.deepEqual(result, [[1, 'John']]); +}); + +test.serial('insert via db.get', async (t) => { + const { db } = t.context; + + const inserted = await db.get( + sql`insert into ${usersTable} (${ + name(usersTable.name.name) + }) values (${'John'}) returning ${usersTable.id}, ${usersTable.name}`, + ); + t.deepEqual(inserted, [1, 'John']); +}); + +test.serial('insert via db.run + select via db.get', async (t) => { + const { db } = t.context; + + await db.run(sql`insert into ${usersTable} (${name(usersTable.name.name)}) values (${'John'})`); + + const result = await db.get( + sql`select ${usersTable.id}, ${usersTable.name} from ${usersTable}`, + ); + t.deepEqual(result, [1, 'John']); +}); + +test.serial('insert via db.get w/ query builder', async (t) => { + const { db } = t.context; + + const inserted = await db.get( + db.insert(usersTable).values({ name: 'John' }).returning({ id: usersTable.id, name: usersTable.name }), + ); + t.deepEqual(inserted, [1, 'John']); +}); + +test.after.always((t) => { + const ctx = t.context; + ctx.client?.close(); +});