Skip to content

Commit

Permalink
add hasDefaultValue and isAutoIncrementing columns for ColumnMetadata
Browse files Browse the repository at this point in the history
  • Loading branch information
koskimas committed Jun 28, 2022
1 parent ae50722 commit 68a69ab
Show file tree
Hide file tree
Showing 10 changed files with 182 additions and 18 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "kysely",
"version": "0.19.5",
"version": "0.19.6",
"description": "Type safe SQL query builder",
"repository": {
"type": "git",
Expand Down
12 changes: 11 additions & 1 deletion src/dialect/database-introspector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
9 changes: 7 additions & 2 deletions src/dialect/mysql/mysql-introspector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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<RawColumnMetadata>()
Expand Down Expand Up @@ -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,
})
)

Expand All @@ -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
}
18 changes: 16 additions & 2 deletions src/dialect/postgres/postgres-introspector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<any>
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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,
})
)

Expand All @@ -107,5 +119,7 @@ interface RawColumnMetadata {
table: string
schema: string
not_null: boolean
type: ColumnDataType
has_default: boolean
type: string
auto_incrementing: boolean | null
}
25 changes: 21 additions & 4 deletions src/dialect/sqlite/sqlite-introspector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -56,15 +55,31 @@ export class SqliteIntrospector implements DatabaseIntrospector {
async #getTableMetadata(table: string): Promise<TableMetadata> {
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 {
Expand All @@ -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,
})),
}
}
Expand Down
14 changes: 14 additions & 0 deletions src/query-builder/join-builder.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -180,6 +181,19 @@ export class JoinBuilder<DB, TB extends keyof DB>
})
}

/**
* Adds `on true`.
*/
onTrue(): JoinBuilder<DB, TB> {
return new JoinBuilder({
...this.#props,
joinNode: JoinNode.cloneWithOn(
this.#props.joinNode,
RawNode.createWithSql('true')
),
})
}

toOperationNode(): JoinNode {
return this.#props.joinNode
}
Expand Down
30 changes: 30 additions & 0 deletions src/query-builder/select-query-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -900,6 +900,21 @@ export class SelectQueryBuilder<DB, TB extends keyof DB, O>

/**
* 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<DB, TB>,
Expand All @@ -924,6 +939,21 @@ export class SelectQueryBuilder<DB, TB extends keyof DB, O>

/**
* 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<DB, TB>,
Expand Down
4 changes: 2 additions & 2 deletions test/node/src/join.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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')
Expand Down
4 changes: 2 additions & 2 deletions test/node/src/migration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -568,8 +568,8 @@ for (const dialect of BUILT_IN_DIALECTS) {
tableName: string,
schema?: string
): Promise<boolean> {
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)
)
}
Expand Down
82 changes: 78 additions & 4 deletions test/node/src/schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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),',
Expand All @@ -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',
})
})
}

Expand Down Expand Up @@ -1569,8 +1643,8 @@ for (const dialect of BUILT_IN_DIALECTS) {

async function getColumnMeta(ref: string): Promise<ColumnMetadata> {
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)!
}
})
Expand Down

0 comments on commit 68a69ab

Please sign in to comment.