-
Notifications
You must be signed in to change notification settings - Fork 280
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
Fail to cast boolean column: SQLite3 can only bind numbers, strings, bigints, buffers, and null #123
Comments
Update: it wasn't the date column, it was the boolean column. The error is related to WiseLibs/better-sqlite3#258 Could be possible to implemented an automatic casting mechanism inside Kysely? |
It's possible to some extent but shouldn't be done inside Kysely. The reason is the solution involves loading the database metadata (to know which columns are bool) and a query builder shouldn't run unexpected queries in the background. We could provide an optional plugin, but implementing it in a generic way is hard. Consider this query: select aa.x as y from b inner join a as aa on aa.id = b.id The plugin would need to figure out where the The plugin also wouldn't work in all cases. Any raw SQL in a select/from/join statement would break the data type inference since kysely doesn't have an SQL parser. |
Had similar concerns with in-and-out casting. Was looking into adding a plugin, saw it'll require too much work. Ended up using import { Kysely } from 'kysely'
import { z } from 'zod'
const taskWriteSchema = z
.object({ /* define your javascript columns here, without generated columns */ })
.transform(value => { /* do your casting here so the driver won't yell at you */ })
const taskReadSchema = taskWriteSchema
.extend({ /* define columns you've casted in write schema and generated columns */ })
.transform(value => { /* do your casting here so you can work with what the driver returned with ease */ })
export type WritableTask = z.input<typeof taskWriteSchema>
export type PersistedTask = z.input<typeof taskReadSchema>
export type Task = z.output<typeof taskReadSchema>
export type Database = {
tasks: PersistedTask
}
const db = new Kysely<Database>(...)
async function insert(tasks: WriteableTask[]): Promise<void> {
await db.insertInto('tasks').values(tasks.map(task => taskWriteSchema.parse(task))).execute()
}
async function readOne(id: Task['id']): Promise<void> {
const result = await db.selectFrom('tasks').where('id', '=', id).selectAll().executeTakeFirst()
return taskReadSchema.parse(result);
} Transforming the entire object is not required, you can also transform per column. |
I considered adding the plugin, but I have to agree with @igalklebanov that it's better to do the mapping outside kysely. A generic plugin would require too much work and have too many broken corner cases. You could create your own plugin that could work nicely in your use cases. The plugin interface is simple and there are a couple of plugins you can use as a starting point here. |
This already does the conversion in the other direction: export class SqliteBooleanPlugin implements KyselyPlugin {
readonly #transformer = new SqliteBooleanTransformer()
transformQuery(args: PluginTransformQueryArgs): RootOperationNode {
return this.#transformer.transformNode(args.node)
}
transformResult(
args: PluginTransformResultArgs
): Promise<QueryResult<UnknownRow>> {
return Promise.resolve(args.result)
}
}
class SqliteBooleanTransformer extends OperationNodeTransformer {
transformValue(node: ValueNode): ValueNode {
return {
...super.transformValue(node),
value: typeof node.value === 'boolean' ? (node.value ? 1 : 0) : node.value
}
}
} The other direction is the tricky one. |
@igalklebanov I use branded types SqliteBoolean and SqliteDateTime with cast helper. /**
* SQLite has no Boolean datatype. Use cast(true|false|SqliteBoolean).
* https://www.sqlite.org/quirks.html#no_separate_boolean_datatype
*/
export const SqliteBoolean = z
.number()
.refine(brand("SqliteBoolean", (n) => n === 0 || n === 1));
export type SqliteBoolean = z.infer<typeof SqliteBoolean>;
/**
* SQLite has no DateTime datatype. Use cast(new Date()|SqliteDateTime).
* https://www.sqlite.org/quirks.html#no_separate_datetime_datatype
*/
export const SqliteDateTime = z
.string()
.refine(brand("SqliteDateTime", (s) => !isNaN(new Date(s).getTime())));
export type SqliteDateTime = z.infer<typeof SqliteDateTime>;
export function cast(value: boolean): SqliteBoolean;
export function cast(value: SqliteBoolean): boolean;
export function cast(value: Date): SqliteDateTime;
export function cast(value: SqliteDateTime): Date;
export function cast(
value: boolean | SqliteBoolean | Date | SqliteDateTime
): boolean | SqliteBoolean | Date | SqliteDateTime {
if (typeof value === "boolean")
return (value === true ? 1 : 0) as SqliteBoolean;
if (typeof value === "number") return value === 1;
if (value instanceof Date) return value.toISOString() as SqliteDateTime;
return new Date(value);
} |
I'm facing similar issues with querying against a |
@koskimas It would be great to hear your opinion on this approach: https://github.com/mphill/kysely-expo#date-and-boolean-support It's a little bit of magic, but it's also straightforward. @nikeee you may be interested too. |
I ran into this issue just now as well since I swapped from PlanetScale to Turso (which uses libSQL, which is a fork of SQLite). I decided to fork their driver and secretly add another property: I then wrote an interface override to allow that property in import { type KyselyPlugin } from 'kysely';
export const sqliteBooleanPlugin: KyselyPlugin = {
transformQuery: args => args.node,
// eslint-disable-next-line @typescript-eslint/require-await
transformResult: async args => {
const indices = [];
for (let i = 0; i < args.result.columnDecltypes.length; i++) {
if (args.result.columnDecltypes[i] === 'boolean') {
indices.push(i);
}
}
if (indices.length === 0) {
return args.result;
}
for (const row of args.result.rows) {
const keys = Object.keys(row);
for (const index of indices) {
row[keys[index]!] = Boolean(row[keys[index]!]);
}
}
return args.result;
},
}; And it seems to be working! The fork README includes instructions on how to accomplish it from scratch. Hope this helps someone! |
I got that error because one table has a
dateboolean column, the following is the code that I'm usingStacktrace
Is this error caused by a limitation of the driver?
how can I cast the boolean to number and back to boolean using Kysely?
Dependency versions
better-sqlite3 v7.6.2
kysely v0.19.12
typescript v4.7.4
OS
Manjaro 21.3.4 x86_64
Node.js 16.15.1
The text was updated successfully, but these errors were encountered: