From 68a69abdf3ee49d7be784eb027a9cfeb4752a58c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sami=20Koskim=C3=A4ki?= Date: Tue, 28 Jun 2022 23:03:21 +0300 Subject: [PATCH] add hasDefaultValue and isAutoIncrementing columns for ColumnMetadata --- package.json | 2 +- src/dialect/database-introspector.ts | 12 ++- src/dialect/mysql/mysql-introspector.ts | 9 +- src/dialect/postgres/postgres-introspector.ts | 18 +++- src/dialect/sqlite/sqlite-introspector.ts | 25 +++++- src/query-builder/join-builder.ts | 14 ++++ src/query-builder/select-query-builder.ts | 30 +++++++ test/node/src/join.test.ts | 4 +- test/node/src/migration.test.ts | 4 +- test/node/src/schema.test.ts | 82 ++++++++++++++++++- 10 files changed, 182 insertions(+), 18 deletions(-) diff --git a/package.json b/package.json index 5c8b985f0..5291ebab5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "kysely", - "version": "0.19.5", + "version": "0.19.6", "description": "Type safe SQL query builder", "repository": { "type": "git", diff --git a/src/dialect/database-introspector.ts b/src/dialect/database-introspector.ts index 3f3b4db15..e5de2ae53 100644 --- a/src/dialect/database-introspector.ts +++ b/src/dialect/database-introspector.ts @@ -49,6 +49,16 @@ export interface TableMetadata { export interface ColumnMetadata { readonly name: string - readonly dataType: ColumnDataType + /** + * The data type of the column as reported by the database. + * + * NOTE: This value is whatever the database engine returns and it will be + * different on different dialects even if you run the same migrations. + * For example `integer` datatype in a migration will produce `int4` + * on PostgreSQL, `INTEGER` on SQLite and `int` on MySQL. + */ + readonly dataType: string + readonly isAutoIncrementing: boolean readonly isNullable: boolean + readonly hasDefaultValue: boolean } diff --git a/src/dialect/mysql/mysql-introspector.ts b/src/dialect/mysql/mysql-introspector.ts index f666face0..99f118027 100644 --- a/src/dialect/mysql/mysql-introspector.ts +++ b/src/dialect/mysql/mysql-introspector.ts @@ -10,7 +10,6 @@ import { DEFAULT_MIGRATION_TABLE, } from '../../migration/migrator.js' import { Kysely } from '../../kysely.js' -import { ColumnDataType } from '../../operation-node/data-type-node.js' import { freeze } from '../../util/object-utils.js' import { sql } from '../../raw-builder/sql.js' @@ -38,10 +37,12 @@ export class MysqlIntrospector implements DatabaseIntrospector { .selectFrom('information_schema.columns') .select([ 'column_name', + 'column_default', 'table_name', 'table_schema', 'is_nullable', 'data_type', + 'extra', ]) .where('table_schema', '=', sql`database()`) .castTo() @@ -83,6 +84,8 @@ export class MysqlIntrospector implements DatabaseIntrospector { name: it.COLUMN_NAME, dataType: it.DATA_TYPE, isNullable: it.IS_NULLABLE === 'YES', + isAutoIncrementing: it.EXTRA.toLowerCase().includes('auto_increment'), + hasDefaultValue: it.COLUMN_DEFAULT !== null, }) ) @@ -97,8 +100,10 @@ interface RawSchemaMetadata { interface RawColumnMetadata { COLUMN_NAME: string + COLUMN_DEFAULT: any TABLE_NAME: string TABLE_SCHEMA: string IS_NULLABLE: 'YES' | 'NO' - DATA_TYPE: ColumnDataType + DATA_TYPE: string + EXTRA: string } diff --git a/src/dialect/postgres/postgres-introspector.ts b/src/dialect/postgres/postgres-introspector.ts index 04bba3d93..24cf03387 100644 --- a/src/dialect/postgres/postgres-introspector.ts +++ b/src/dialect/postgres/postgres-introspector.ts @@ -10,8 +10,8 @@ import { DEFAULT_MIGRATION_TABLE, } from '../../migration/migrator.js' import { Kysely } from '../../kysely.js' -import { ColumnDataType } from '../../operation-node/data-type-node.js' import { freeze } from '../../util/object-utils.js' +import { sql } from '../../raw-builder/sql.js' export class PostgresIntrospector implements DatabaseIntrospector { readonly #db: Kysely @@ -41,9 +41,19 @@ export class PostgresIntrospector implements DatabaseIntrospector { .select([ 'a.attname as column', 'a.attnotnull as not_null', + 'a.atthasdef as has_default', 't.tablename as table', 't.schemaname as schema', 'typ.typname as type', + + // Detect if the column is auto incrementing by finding the sequence + // that is created for `serial` and `bigserial` columns. + this.#db + .selectFrom('pg_class') + .select(sql`true`.as('auto_incrementing')) + .where('relkind', '=', 'S') + .where('relname', '=', sql`t.tablename || '_' || a.attname || '_seq'`) + .as('auto_incrementing'), ]) .where('t.schemaname', '!~', '^pg_') .where('t.schemaname', '!=', 'information_schema') @@ -90,6 +100,8 @@ export class PostgresIntrospector implements DatabaseIntrospector { name: it.column, dataType: it.type, isNullable: !it.not_null, + isAutoIncrementing: !!it.auto_incrementing, + hasDefaultValue: it.has_default, }) ) @@ -107,5 +119,7 @@ interface RawColumnMetadata { table: string schema: string not_null: boolean - type: ColumnDataType + has_default: boolean + type: string + auto_incrementing: boolean | null } diff --git a/src/dialect/sqlite/sqlite-introspector.ts b/src/dialect/sqlite/sqlite-introspector.ts index 97d412681..7b1d15da5 100644 --- a/src/dialect/sqlite/sqlite-introspector.ts +++ b/src/dialect/sqlite/sqlite-introspector.ts @@ -10,7 +10,6 @@ import { DEFAULT_MIGRATION_LOCK_TABLE, DEFAULT_MIGRATION_TABLE, } from '../../migration/migrator.js' -import { ColumnDataType } from '../../operation-node/data-type-node.js' import { sql } from '../../raw-builder/sql.js' export class SqliteIntrospector implements DatabaseIntrospector { @@ -56,15 +55,31 @@ export class SqliteIntrospector implements DatabaseIntrospector { async #getTableMetadata(table: string): Promise { const db = this.#db + // Get the SQL that was used to create the table. + const createSql = await db + .selectFrom('sqlite_master') + .where('name', '=', table) + .select('sql') + .castTo<{ sql: string | undefined }>() + .execute() + + // Try to find the name of the column that has `autoincrement` 🤦 + const autoIncrementCol = createSql[0]?.sql + ?.split(/[\(\),]/) + ?.find((it) => it.toLowerCase().includes('autoincrement')) + ?.split(/\s+/)?.[0] + ?.replaceAll(/["`]/g, '') + const columns = await db .selectFrom( sql<{ name: string - type: ColumnDataType + type: string notnull: 0 | 1 - }>`PRAGMA_TABLE_INFO(${table})`.as('table_info') + dflt_value: any + }>`pragma_table_info(${table})`.as('table_info') ) - .select(['name', 'type', 'notnull']) + .select(['name', 'type', 'notnull', 'dflt_value']) .execute() return { @@ -73,6 +88,8 @@ export class SqliteIntrospector implements DatabaseIntrospector { name: col.name, dataType: col.type, isNullable: !col.notnull, + isAutoIncrementing: col.name === autoIncrementCol, + hasDefaultValue: !!col.dflt_value, })), } } diff --git a/src/query-builder/join-builder.ts b/src/query-builder/join-builder.ts index be2f2ad02..ead9ca02c 100644 --- a/src/query-builder/join-builder.ts +++ b/src/query-builder/join-builder.ts @@ -1,5 +1,6 @@ import { JoinNode } from '../operation-node/join-node.js' import { OperationNodeSource } from '../operation-node/operation-node-source.js' +import { RawNode } from '../operation-node/raw-node.js' import { ExistsExpression, FilterOperator, @@ -180,6 +181,19 @@ export class JoinBuilder }) } + /** + * Adds `on true`. + */ + onTrue(): JoinBuilder { + return new JoinBuilder({ + ...this.#props, + joinNode: JoinNode.cloneWithOn( + this.#props.joinNode, + RawNode.createWithSql('true') + ), + }) + } + toOperationNode(): JoinNode { return this.#props.joinNode } diff --git a/src/query-builder/select-query-builder.ts b/src/query-builder/select-query-builder.ts index 828fc1528..40af8334c 100644 --- a/src/query-builder/select-query-builder.ts +++ b/src/query-builder/select-query-builder.ts @@ -900,6 +900,21 @@ export class SelectQueryBuilder /** * Just like {@link innerJoin} but adds a lateral join instead of an inner join. + * + * ### Examples + * + * ```ts + * db.selectFrom('person') + * .innerJoinLateral( + * (eb) => + * eb.selectFrom('pet') + * .select('name') + * .whereRef('pet.owner_id', '=', 'person.id') + * .as('p'), + * (join) => join.onTrue() + * ) + * .select(['first_name', 'p.name']) + * .orderBy('first_name') */ innerJoinLateral< TE extends TableExpression, @@ -924,6 +939,21 @@ export class SelectQueryBuilder /** * Just like {@link innerJoin} but adds a lateral left join instead of an inner join. + * ### Examples + * + * ```ts + * db.selectFrom('person') + * .leftJoinLateral( + * (eb) => + * eb.selectFrom('pet') + * .select('name') + * .whereRef('pet.owner_id', '=', 'person.id') + * .as('p'), + * (join) => join.onTrue() + * ) + * .select(['first_name', 'p.name']) + * .orderBy('first_name') + * ``` */ leftJoinLateral< TE extends TableExpression, diff --git a/test/node/src/join.test.ts b/test/node/src/join.test.ts index 76c032947..96c041e90 100644 --- a/test/node/src/join.test.ts +++ b/test/node/src/join.test.ts @@ -587,7 +587,7 @@ for (const dialect of BUILT_IN_DIALECTS) { .select('name') .whereRef('pet.owner_id', '=', 'person.id') .as('p'), - (join) => join.on(sql`true`) + (join) => join.onTrue() ) .select(['first_name', 'p.name']) .orderBy('first_name') @@ -619,7 +619,7 @@ for (const dialect of BUILT_IN_DIALECTS) { .select('name') .whereRef('pet.owner_id', '=', 'person.id') .as('p'), - (join) => join.on(sql`true`) + (join) => join.onTrue() ) .select(['first_name', 'p.name']) .orderBy('first_name') diff --git a/test/node/src/migration.test.ts b/test/node/src/migration.test.ts index 194dc6003..b09589ac6 100644 --- a/test/node/src/migration.test.ts +++ b/test/node/src/migration.test.ts @@ -568,8 +568,8 @@ for (const dialect of BUILT_IN_DIALECTS) { tableName: string, schema?: string ): Promise { - const metadata = await ctx.db.introspection.getMetadata() - return !!metadata.tables.find( + const tables = await ctx.db.introspection.getTables() + return !!tables.find( (it) => it.name === tableName && (!schema || it.schema === schema) ) } diff --git a/test/node/src/schema.test.ts b/test/node/src/schema.test.ts index 95275cb80..63d4e9bbc 100644 --- a/test/node/src/schema.test.ts +++ b/test/node/src/schema.test.ts @@ -98,6 +98,30 @@ for (const dialect of BUILT_IN_DIALECTS) { }) await builder.execute() + + expect(await getColumnMeta('test.a')).to.eql({ + dataType: 'int4', + isAutoIncrementing: true, + isNullable: false, + hasDefaultValue: true, + name: 'a', + }) + + expect(await getColumnMeta('test.b')).to.eql({ + dataType: 'int4', + isAutoIncrementing: false, + isNullable: true, + hasDefaultValue: false, + name: 'b', + }) + + expect(await getColumnMeta('test.l')).to.eql({ + dataType: 'bool', + isAutoIncrementing: false, + isNullable: false, + hasDefaultValue: true, + name: 'l', + }) }) } else if (dialect === 'mysql') { it('should create a table with all data types', async () => { @@ -159,12 +183,38 @@ for (const dialect of BUILT_IN_DIALECTS) { }) await builder.execute() + + expect(await getColumnMeta('test.a')).to.eql({ + dataType: 'int', + isAutoIncrementing: true, + isNullable: false, + hasDefaultValue: false, + name: 'a', + }) + + expect(await getColumnMeta('test.b')).to.eql({ + dataType: 'int', + isAutoIncrementing: false, + isNullable: true, + hasDefaultValue: false, + name: 'b', + }) + + expect(await getColumnMeta('test.k')).to.eql({ + dataType: 'tinyint', + isAutoIncrementing: false, + isNullable: false, + hasDefaultValue: true, + name: 'k', + }) }) } else { it('should create a table with all data types', async () => { const builder = ctx.db.schema .createTable('test') - .addColumn('a', 'serial', (col) => col.primaryKey()) + .addColumn('a', 'integer', (col) => + col.primaryKey().autoIncrement().notNull() + ) .addColumn('b', 'integer', (col) => col .references('test.a') @@ -199,7 +249,7 @@ for (const dialect of BUILT_IN_DIALECTS) { sqlite: { sql: [ 'create table "test"', - '("a" serial primary key,', + '("a" integer not null primary key autoincrement,', '"b" integer references "test" ("a") on delete cascade on update restrict check (b < a),', '"c" varchar,', '"d" varchar(10),', @@ -226,6 +276,30 @@ for (const dialect of BUILT_IN_DIALECTS) { }) await builder.execute() + + expect(await getColumnMeta('test.a')).to.eql({ + dataType: 'INTEGER', + isAutoIncrementing: true, + isNullable: false, + hasDefaultValue: false, + name: 'a', + }) + + expect(await getColumnMeta('test.b')).to.eql({ + dataType: 'INTEGER', + isAutoIncrementing: false, + isNullable: true, + hasDefaultValue: false, + name: 'b', + }) + + expect(await getColumnMeta('test.l')).to.eql({ + dataType: 'boolean', + isAutoIncrementing: false, + isNullable: false, + hasDefaultValue: true, + name: 'l', + }) }) } @@ -1569,8 +1643,8 @@ for (const dialect of BUILT_IN_DIALECTS) { async function getColumnMeta(ref: string): Promise { const [table, column] = ref.split('.') - const meta = await ctx.db.introspection.getMetadata() - const tableMeta = meta.tables.find((it) => it.name === table) + const tables = await ctx.db.introspection.getTables() + const tableMeta = tables.find((it) => it.name === table) return tableMeta!.columns.find((it) => it.name === column)! } })