Skip to content
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

Add support for boolean datatype #43

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/cuddly-dodos-hope.md
@@ -0,0 +1,5 @@
---
"d1-orm": minor
---

Support for boolean datatype
8 changes: 8 additions & 0 deletions guides/models.md
Expand Up @@ -42,6 +42,10 @@ const users = new Model<User>(
type: DataTypes.STRING,
unique: true,
},
validated: {
type: DataTypes.BOOLEAN,
defaultValue: false,
},
}
);
```
Expand Down Expand Up @@ -76,6 +80,8 @@ That's it! You've now created a model. You can now use the model to query the da

There are two ways of selecting data from the database. The first is to use the {@link Model.First} method which will return one result, and the second is to use the {@link Model.All} method, which will return an array of results.

All columns specified in the model as `DataTypes.BOOLEAN` will be stored in the database as `false = 0, true = 1`. Data returned will be returned typed as an integer. What this means is that if executing an equality test, an if statement will operate exactly as intended if you use a `==`. This is because a zero is a falsey value and one is a truthy value and the actual data type is not being compared. Attempting to test boolean equality with `===` will not yield the desired result as this equality check first checks the data type of the value and boolean != integer.

#### First()

Let's start with the {@link Model.First} method. This method will return the first result that matches the query. It takes a single argument, which is an object containing a `Where` clause. See [Query Building](/guides/query-building) for more information on how to use the `Where` clause. This should be an object with a key of the column name, and a value of the value to match.
Expand Down Expand Up @@ -105,6 +111,8 @@ This will return the first 10 users with a name of "John Doe", ordered by ID, eq

There are two methods used to insert data into the database. The first is {@link Model.InsertOne}, which will insert a single row, and the second is {@link Model.InsertMany}, which will insert multiple rows. Both accept an optional boolean parameter instructing the [Query Building](/guides/query-building) to generate `INSERT or REPLACE` instead of just `INSERT`. This mechanism differs from [Upsert](/guides/upserting) by its requirement of only replacing records based on the primary key.

All columns specified in the model as `DataTypes.BOOLEAN` will be stored in the database as integer values where `false = 0, true = 1`.

#### InsertOne()

This method takes just one parameter, which is the data to insert. This should be an object with a key of the column name, and a value of the value to insert. For example:
Expand Down
4 changes: 4 additions & 0 deletions src/datatypes.ts
@@ -1,9 +1,13 @@
/**
* SQLite specification doesn't provide an explicit boolean data type,
* so boolean is converted to an integer where: 0=false, 1=true
*
* @enum {string} Aliases for DataTypes used in a {@link ModelColumn} definition.
*/
export enum DataTypes {
INTEGER = "integer",
INT = "integer",
BOOLEAN = "boolean",
TEXT = "text",
STRING = "text",
VARCHAR = "text",
Expand Down
14 changes: 12 additions & 2 deletions src/model.ts
Expand Up @@ -79,7 +79,9 @@ export class Model<T extends object> {
const columnEntries = Object.entries(this.columns);
let hasAutoIncrement = false;
const columnDefinition = columnEntries.map(([columnName, column]) => {
let definition = `${columnName} ${column.type}`;
console.log(column.type);
const ct = column.type === DataTypes.BOOLEAN ? "integer" : column.type;
let definition = `${columnName} ${ct}`;
if (column.autoIncrement) {
hasAutoIncrement = true;
definition += " PRIMARY KEY AUTOINCREMENT";
Expand All @@ -91,7 +93,7 @@ export class Model<T extends object> {
definition += " UNIQUE";
}
if (column.defaultValue !== undefined) {
definition += ` DEFAULT "${column.defaultValue}"`;
definition += ` DEFAULT "${this.coerceTypedValue(column)}"`;
}
return definition;
});
Expand All @@ -102,6 +104,14 @@ export class Model<T extends object> {
)});`;
}

private coerceTypedValue(column: ModelColumn): unknown {
if (column.type === DataTypes.BOOLEAN) {
return Boolean(column.defaultValue).valueOf() ? 1 : 0;
} else {
return column.defaultValue;
}
}

/**
* @param options The options for creating the table. Currently only contains strategy, which is the strategy to use when creating the table.
* - "default" - The default strategy, which will attempt create the table.
Expand Down
28 changes: 22 additions & 6 deletions src/queryBuilder.ts
Expand Up @@ -75,7 +75,7 @@ export function GenerateQuery<T extends object>(
const whereStmt = [];
for (const [key, value] of Object.entries(options.where)) {
whereStmt.push(`${key} = ?`);
bindings.push(value);
bindings.push(coerceTypedValue(value));
}
if (whereStmt.length) query += ` WHERE ${whereStmt.join(" AND ")}`;
}
Expand All @@ -96,7 +96,7 @@ export function GenerateQuery<T extends object>(
const whereStmt = [];
for (const [key, value] of Object.entries(options.where)) {
whereStmt.push(`${key} = ?`);
bindings.push(value);
bindings.push(coerceTypedValue(value));
}
if (whereStmt.length) query += ` WHERE ${whereStmt.join(" AND ")}`;
}
Expand All @@ -114,7 +114,7 @@ export function GenerateQuery<T extends object>(
const keys = [];
for (const [key, value] of Object.entries(options.data)) {
keys.push(key);
bindings.push(value);
bindings.push(coerceTypedValue(value));
}
query += ` (${keys.join(", ")}) VALUES (${"?"
.repeat(keys.length)
Expand All @@ -133,14 +133,14 @@ export function GenerateQuery<T extends object>(
const keys = [];
for (const [key, value] of Object.entries(options.data)) {
keys.push(`${key} = ?`);
bindings.push(value);
bindings.push(coerceTypedValue(value));
}
query += ` SET ${keys.join(", ")}`;
if (options.where) {
const whereStmt = [];
for (const [key, value] of Object.entries(options.where)) {
whereStmt.push(`${key} = ?`);
bindings.push(value);
bindings.push(coerceTypedValue(value));
}
if (whereStmt.length) query += ` WHERE ${whereStmt.join(" AND ")}`;
}
Expand All @@ -155,7 +155,7 @@ export function GenerateQuery<T extends object>(
...Object.values(options.upsertOnlyUpdateData ?? {}),
...Object.values(options.where ?? {})
);

coerceTypedValues(bindings);
if (
insertDataKeys.length === 0 ||
updateDataKeys.length === 0 ||
Expand Down Expand Up @@ -188,6 +188,22 @@ export function GenerateQuery<T extends object>(
};
}

/** @hidden */
export function coerceTypedValues(list: Array<unknown>) {
for (let i = 0; i < list.length; i++) {
list[i] = coerceTypedValue(list[i]);
}
}

/** @hidden */
export function coerceTypedValue(value: unknown) {
Skye-31 marked this conversation as resolved.
Show resolved Hide resolved
if (typeof value === "boolean") {
return value ? 1 : 0;
} else {
return value;
}
}

/**
* @private
* @hidden
Expand Down
48 changes: 48 additions & 0 deletions test/model.test.js
Expand Up @@ -155,4 +155,52 @@ describe("Model > Create Tables", () => {
'CREATE TABLE `test` (id integer, name text DEFAULT "test", PRIMARY KEY (id));'
);
});
it("should support a default value for boolean string true", () => {
const model = new Model(
{ D1Orm: orm, tableName: "test" },
{
id: { type: DataTypes.INTEGER, primaryKey: true },
flag: { type: DataTypes.BOOLEAN, defaultValue: "true" },
}
);
expect(model.createTableDefinition).to.equal(
'CREATE TABLE `test` (id integer, flag integer DEFAULT "1", PRIMARY KEY (id));'
);
});
it("should support a default value for boolean false", () => {
const model = new Model(
{ D1Orm: orm, tableName: "test" },
{
id: { type: DataTypes.INTEGER, primaryKey: true },
flag: { type: DataTypes.BOOLEAN, defaultValue: false },
}
);
expect(model.createTableDefinition).to.equal(
'CREATE TABLE `test` (id integer, flag integer DEFAULT "0", PRIMARY KEY (id));'
);
});
it("should support a default value for integer 0 false", () => {
const model = new Model(
{ D1Orm: orm, tableName: "test" },
{
id: { type: DataTypes.INTEGER, primaryKey: true },
flag: { type: DataTypes.BOOLEAN, defaultValue: 0 },
}
);
expect(model.createTableDefinition).to.equal(
'CREATE TABLE `test` (id integer, flag integer DEFAULT "0", PRIMARY KEY (id));'
);
});
it("should support a default value for integer 1 true", () => {
const model = new Model(
{ D1Orm: orm, tableName: "test" },
{
id: { type: DataTypes.INTEGER, primaryKey: true },
flag: { type: DataTypes.BOOLEAN, defaultValue: 1 },
}
);
expect(model.createTableDefinition).to.equal(
'CREATE TABLE `test` (id integer, flag integer DEFAULT "1", PRIMARY KEY (id));'
);
});
});
20 changes: 12 additions & 8 deletions test/querybuilder.test.js
Expand Up @@ -167,14 +167,16 @@ describe("Query Builder", () => {
});
it("should generate a query with multiple columns", () => {
const statement = GenerateQuery(QueryType.INSERT, "test", {
data: { id: 1, name: "test" },
data: { id: 1, name: "test", flagA: true, flagB: false },
});
expect(statement.query).to.equal(
"INSERT INTO `test` (id, name) VALUES (?, ?)"
"INSERT INTO `test` (id, name, flagA, flagB) VALUES (?, ?, ?, ?)"
);
expect(statement.bindings.length).to.equal(2);
expect(statement.bindings.length).to.equal(4);
expect(statement.bindings[0]).to.equal(1);
expect(statement.bindings[1]).to.equal("test");
expect(statement.bindings[2]).to.equal(1);
expect(statement.bindings[3]).to.equal(0);
});
});
describe(QueryType.INSERT_OR_REPLACE, () => {
Expand Down Expand Up @@ -294,17 +296,18 @@ describe("Query Builder", () => {
});
it("should generate a basic query", () => {
const statement = GenerateQuery(QueryType.UPSERT, "test", {
data: { id: 1 },
data: { id: 1, flag: true },
upsertOnlyUpdateData: { id: 2 },
where: { id: 3 },
});
expect(statement.query).to.equal(
"INSERT INTO `test` (id) VALUES (?) ON CONFLICT (id) DO UPDATE SET id = ? WHERE id = ?"
"INSERT INTO `test` (id, flag) VALUES (?, ?) ON CONFLICT (id) DO UPDATE SET id = ? WHERE id = ?"
);
expect(statement.bindings.length).to.equal(3);
expect(statement.bindings.length).to.equal(4);
expect(statement.bindings[0]).to.equal(1);
expect(statement.bindings[1]).to.equal(2);
expect(statement.bindings[2]).to.equal(3);
expect(statement.bindings[1]).to.equal(1);
expect(statement.bindings[2]).to.equal(2);
expect(statement.bindings[3]).to.equal(3);
});
it("should generate a query with multiple columns", () => {
const statement = GenerateQuery(QueryType.UPSERT, "test", {
Expand All @@ -326,6 +329,7 @@ describe("Query Builder", () => {
const statement = GenerateQuery(
QueryType.UPSERT,
"test",

kingmesal marked this conversation as resolved.
Show resolved Hide resolved
{
data: { id: 1, name: "test" },
upsertOnlyUpdateData: { id: 1, name: "test" },
Expand Down