@bunnykit/orm 0.1.21 → 0.1.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,6 +9,7 @@ export declare class Connection {
9
9
  private schema?;
10
10
  private ownsDriver;
11
11
  private transactionDepth;
12
+ private savepointId;
12
13
  constructor(config: ConnectionConfig, options?: {
13
14
  driver?: SQL;
14
15
  schema?: string;
@@ -10,6 +10,7 @@ export class Connection {
10
10
  schema;
11
11
  ownsDriver;
12
12
  transactionDepth = 0;
13
+ savepointId = 0;
13
14
  constructor(config, options = {}) {
14
15
  this.config = config;
15
16
  this.schema = options.schema || ("schema" in config ? config.schema : undefined);
@@ -83,16 +84,36 @@ export class Connection {
83
84
  return await this.driver.unsafe(sqlString, bindings);
84
85
  }
85
86
  async beginTransaction() {
86
- await this.driver.unsafe("BEGIN");
87
+ if (this.transactionDepth === 0) {
88
+ await this.driver.unsafe("BEGIN");
89
+ }
90
+ else {
91
+ await this.driver.unsafe(`SAVEPOINT bunny_trans_${++this.savepointId}`);
92
+ }
87
93
  this.transactionDepth++;
88
94
  }
89
95
  async commit() {
90
- await this.driver.unsafe("COMMIT");
91
- this.transactionDepth = Math.max(0, this.transactionDepth - 1);
96
+ if (this.transactionDepth <= 0)
97
+ return;
98
+ if (this.transactionDepth === 1) {
99
+ await this.driver.unsafe("COMMIT");
100
+ }
101
+ else {
102
+ await this.driver.unsafe(`RELEASE SAVEPOINT bunny_trans_${this.savepointId--}`);
103
+ }
104
+ this.transactionDepth--;
92
105
  }
93
106
  async rollback() {
94
- await this.driver.unsafe("ROLLBACK");
95
- this.transactionDepth = Math.max(0, this.transactionDepth - 1);
107
+ if (this.transactionDepth <= 0)
108
+ return;
109
+ if (this.transactionDepth === 1) {
110
+ await this.driver.unsafe("ROLLBACK");
111
+ }
112
+ else {
113
+ await this.driver.unsafe(`ROLLBACK TO SAVEPOINT bunny_trans_${this.savepointId}`);
114
+ await this.driver.unsafe(`RELEASE SAVEPOINT bunny_trans_${this.savepointId--}`);
115
+ }
116
+ this.transactionDepth--;
96
117
  }
97
118
  isInTransaction() {
98
119
  return this.transactionDepth > 0;
@@ -111,6 +132,9 @@ export class Connection {
111
132
  if (this.driverName !== "postgres") {
112
133
  return await this.transaction(callback);
113
134
  }
135
+ if (!/^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)*$/.test(setting)) {
136
+ throw new Error(`Invalid PostgreSQL setting name: ${setting}`);
137
+ }
114
138
  return await this.transaction(async (connection) => {
115
139
  await connection.run(`SET LOCAL ${setting} = ${connection.getGrammar().placeholder(1)}`, [tenantId]);
116
140
  return await callback(connection);
@@ -195,30 +195,30 @@ export class Migrator {
195
195
  const statements = [];
196
196
  for (const row of tables) {
197
197
  const table = row[key];
198
- const createRows = await this.connection.query(`SHOW CREATE TABLE ${table}`);
198
+ const createRows = await this.connection.query(`SHOW CREATE TABLE ${this.connection.getGrammar().wrap(table)}`);
199
199
  statements.push(`${createRows[0]["Create Table"]};`);
200
200
  }
201
201
  return statements.join("\n\n") + "\n";
202
202
  }
203
203
  const schema = this.connection.getSchema() || "public";
204
- const tables = await this.connection.query(`SELECT table_name FROM information_schema.tables WHERE table_schema = '${schema}' AND table_type = 'BASE TABLE' ORDER BY table_name`);
204
+ const tables = await this.connection.query("SELECT table_name FROM information_schema.tables WHERE table_schema = $1 AND table_type = 'BASE TABLE' ORDER BY table_name", [schema]);
205
205
  const statements = [];
206
206
  for (const tableRow of tables) {
207
207
  const table = tableRow.table_name;
208
208
  const columns = await this.connection.query(`SELECT column_name, data_type, is_nullable, column_default, character_maximum_length, numeric_precision, numeric_scale
209
209
  FROM information_schema.columns
210
- WHERE table_schema = '${schema}' AND table_name = '${table}'
211
- ORDER BY ordinal_position`);
210
+ WHERE table_schema = $1 AND table_name = $2
211
+ ORDER BY ordinal_position`, [schema, table]);
212
212
  const primaryKeys = await this.connection.query(`SELECT kcu.column_name
213
213
  FROM information_schema.table_constraints tc
214
214
  JOIN information_schema.key_column_usage kcu
215
215
  ON tc.constraint_name = kcu.constraint_name
216
216
  AND tc.table_schema = kcu.table_schema
217
217
  AND tc.table_name = kcu.table_name
218
- WHERE tc.table_schema = '${schema}'
219
- AND tc.table_name = '${table}'
218
+ WHERE tc.table_schema = $1
219
+ AND tc.table_name = $2
220
220
  AND tc.constraint_type = 'PRIMARY KEY'
221
- ORDER BY kcu.ordinal_position`);
221
+ ORDER BY kcu.ordinal_position`, [schema, table]);
222
222
  const pkColumns = primaryKeys.map((row) => row.column_name);
223
223
  const columnSql = columns.map((column) => {
224
224
  let type = String(column.data_type).toUpperCase();
@@ -228,7 +228,7 @@ export class Migrator {
228
228
  else if ((type === "NUMERIC" || type === "DECIMAL") && column.numeric_precision) {
229
229
  type = `${type}(${column.numeric_precision}${column.numeric_scale ? `, ${column.numeric_scale}` : ""})`;
230
230
  }
231
- let sql = ` "${column.column_name}" ${type}`;
231
+ let sql = ` ${this.connection.getGrammar().wrap(column.column_name)} ${type}`;
232
232
  if (column.is_nullable === "NO")
233
233
  sql += " NOT NULL";
234
234
  if (column.column_default !== null && column.column_default !== undefined)
@@ -236,9 +236,9 @@ export class Migrator {
236
236
  return sql;
237
237
  });
238
238
  if (pkColumns.length > 0) {
239
- columnSql.push(` PRIMARY KEY (${pkColumns.map((column) => `"${column}"`).join(", ")})`);
239
+ columnSql.push(` PRIMARY KEY (${pkColumns.map((column) => this.connection.getGrammar().wrap(column)).join(", ")})`);
240
240
  }
241
- statements.push(`CREATE TABLE "${schema}"."${table}" (\n${columnSql.join(",\n")}\n);`);
241
+ statements.push(`CREATE TABLE ${this.connection.getGrammar().wrap(`${schema}.${table}`)} (\n${columnSql.join(",\n")}\n);`);
242
242
  }
243
243
  return statements.join("\n\n") + "\n";
244
244
  }
@@ -815,21 +815,22 @@ export class Model {
815
815
  async save() {
816
816
  const constructor = this.constructor;
817
817
  if (this.$exists) {
818
- await ObserverRegistry.dispatch("updating", this);
819
818
  await ObserverRegistry.dispatch("saving", this);
820
- if (constructor.timestamps) {
819
+ let dirty = this.getDirty();
820
+ if (Object.keys(dirty).length > 0 && constructor.timestamps) {
821
821
  this.$attributes["updated_at"] = this.freshTimestamp();
822
+ dirty = this.getDirty();
822
823
  }
823
- const dirty = this.getDirty();
824
824
  if (Object.keys(dirty).length > 0) {
825
+ await ObserverRegistry.dispatch("updating", this);
825
826
  const pk = this.getAttribute(constructor.primaryKey);
826
827
  const connection = this.getConnection();
827
828
  await new Builder(connection, connection.qualifyTable(constructor.getTable()))
828
829
  .where(constructor.primaryKey, pk)
829
830
  .update(dirty);
831
+ await ObserverRegistry.dispatch("updated", this);
830
832
  }
831
833
  this.$original = { ...this.$attributes };
832
- await ObserverRegistry.dispatch("updated", this);
833
834
  await ObserverRegistry.dispatch("saved", this);
834
835
  }
835
836
  else {
@@ -930,10 +931,10 @@ export class Model {
930
931
  }
931
932
  async delete() {
932
933
  const constructor = this.constructor;
933
- await ObserverRegistry.dispatch("deleting", this);
934
934
  const pk = this.getAttribute(constructor.primaryKey);
935
935
  if (!pk)
936
936
  return false;
937
+ await ObserverRegistry.dispatch("deleting", this);
937
938
  if (constructor.softDeletes) {
938
939
  const deletedAt = this.freshTimestamp();
939
940
  const connection = this.getConnection();
@@ -134,7 +134,7 @@ export declare class Builder<T = Record<string, any>> {
134
134
  private addBinding;
135
135
  private compileWhereClause;
136
136
  private compileWheres;
137
- private compileNestedWheres;
137
+ private compileWhereClauses;
138
138
  private compileOrders;
139
139
  private compileGroups;
140
140
  private compileHavings;
@@ -161,11 +161,14 @@ export declare class Builder<T = Record<string, any>> {
161
161
  chunk(count: number, callback: (items: T[]) => void | Promise<void>): Promise<void>;
162
162
  each(count: number, callback: (item: T) => void | Promise<void>): Promise<void>;
163
163
  cursor(chunkSize?: number): AsyncGenerator<T>;
164
+ private getResultAccessColumn;
165
+ private compileCursorWheres;
164
166
  lazy(count?: number): AsyncGenerator<T>;
165
167
  insert(data: ModelAttributeInput<T> | ModelAttributeInput<T>[]): Promise<any>;
166
168
  insertGetId(data: ModelAttributeInput<T>, idColumn?: ModelColumn<T>): Promise<any>;
167
169
  insertOrIgnore(data: ModelAttributeInput<T> | ModelAttributeInput<T>[]): Promise<any>;
168
170
  upsert(data: ModelAttributeInput<T> | ModelAttributeInput<T>[], uniqueBy: ModelColumn<T> | ModelColumn<T>[], updateColumns?: ModelColumn<T>[]): Promise<any>;
171
+ private getUniformColumns;
169
172
  update(data: ModelAttributeInput<T>): Promise<any>;
170
173
  delete(): Promise<any>;
171
174
  increment(column: ModelColumn<T>, amount?: number, extra?: ModelAttributeInput<T>): Promise<any>;
@@ -65,8 +65,7 @@ export class Builder {
65
65
  const nested = new Builder(this.connection, this.tableName);
66
66
  callback(nested);
67
67
  if (nested.wheres.length > 0) {
68
- const sql = this.compileNestedWheres(nested);
69
- this.wheres.push({ type: "raw", column: `(${sql})`, boolean, scope: undefined });
68
+ this.wheres.push({ type: "nested", column: "", query: nested.wheres, boolean, scope: undefined });
70
69
  }
71
70
  return this;
72
71
  }
@@ -484,6 +483,10 @@ export class Builder {
484
483
  else if (where.type === "raw") {
485
484
  return `${prefix} ${where.column}`;
486
485
  }
486
+ else if (where.type === "nested") {
487
+ const sql = this.compileWhereClauses(where.query || [], "");
488
+ return `${prefix} (${sql})`;
489
+ }
487
490
  else if (where.type === "like") {
488
491
  const sql = this.grammar.compileLike(this.grammar.wrap(where.column), where.value, !!where.not, this.parameterize ? (v) => this.addBinding(v) : undefined);
489
492
  return `${prefix} ${sql}`;
@@ -540,20 +543,15 @@ export class Builder {
540
543
  return "";
541
544
  }
542
545
  compileWheres() {
543
- if (this.wheres.length === 0)
544
- return "";
545
- const clauses = this.wheres.map((where, index) => {
546
- const prefix = index === 0 ? "WHERE" : where.boolean.toUpperCase();
547
- return this.compileWhereClause(where, prefix);
548
- });
549
- return clauses.join(" ");
546
+ return this.compileWhereClauses(this.wheres, "WHERE");
550
547
  }
551
- compileNestedWheres(builder) {
552
- if (builder.wheres.length === 0)
548
+ compileWhereClauses(wheres, firstPrefix) {
549
+ if (wheres.length === 0)
553
550
  return "";
554
- const clauses = builder.wheres.map((where, index) => {
555
- const prefix = index === 0 ? "" : where.boolean.toUpperCase();
556
- return this.compileWhereClause(where, prefix);
551
+ const clauses = wheres.map((where, index) => {
552
+ const prefix = index === 0 ? "WHERE" : where.boolean.toUpperCase();
553
+ const adjustedPrefix = index === 0 ? firstPrefix : prefix;
554
+ return this.compileWhereClause(where, adjustedPrefix);
557
555
  });
558
556
  return clauses.join(" ").trim();
559
557
  }
@@ -719,10 +717,15 @@ export class Builder {
719
717
  return results.map((row) => row[column]);
720
718
  }
721
719
  async aggregate(sql, alias) {
722
- const model = this.model;
723
- this.model = undefined;
724
- const result = await this.select(`${sql} as ${alias}`).first();
725
- this.model = model;
720
+ const query = this.clone();
721
+ query.model = undefined;
722
+ query.columns = [`${sql} as ${alias}`];
723
+ query.orders = [];
724
+ query.limitValue = undefined;
725
+ query.offsetValue = undefined;
726
+ query.eagerLoads = [];
727
+ query.lockMode = undefined;
728
+ const result = await query.first();
726
729
  return result ? result[alias] : null;
727
730
  }
728
731
  async count(column = "*") {
@@ -741,7 +744,11 @@ export class Builder {
741
744
  return await this.aggregate(`MAX(${column})`, "max");
742
745
  }
743
746
  async paginate(perPage = 15, page = 1) {
744
- const total = await this.clone().count();
747
+ const countQuery = this.clone();
748
+ countQuery.limitValue = undefined;
749
+ countQuery.offsetValue = undefined;
750
+ countQuery.orders = [];
751
+ const total = await countQuery.count();
745
752
  const data = await this.clone().forPage(page, perPage).get();
746
753
  return {
747
754
  data,
@@ -781,9 +788,7 @@ export class Builder {
781
788
  }
782
789
  const orderColumn = this.orders[0]?.column || primaryKey;
783
790
  const orderDirection = this.orders[0]?.direction || "asc";
784
- // Use unqualified column name for property access on model instances
785
- const accessColumn = orderColumn.includes(".") ? orderColumn.split(".")[1] : orderColumn;
786
- let lastValue = undefined;
791
+ let lastValues = undefined;
787
792
  while (true) {
788
793
  const builder = this.clone();
789
794
  // Preserve multi-column ORDER BY, appending PK tie-breaker if not present
@@ -794,23 +799,15 @@ export class Builder {
794
799
  }
795
800
  builder.offsetValue = undefined;
796
801
  builder.limitValue = chunkSize;
797
- if (lastValue !== undefined) {
798
- const op = orderDirection === "asc" ? ">" : "<";
802
+ if (lastValues !== undefined) {
799
803
  // Parenthesize existing wheres when appending cursor condition to preserve OR precedence
800
804
  if (builder.wheres.length > 0) {
801
805
  const hasOr = builder.wheres.some((w) => w.boolean === "or");
802
806
  if (hasOr) {
803
- builder.wheres = [{ type: "raw", column: `(${builder.compileWheres().replace(/^WHERE /, "")})`, boolean: "and", scope: undefined }];
807
+ builder.wheres = [{ type: "nested", column: "", query: builder.wheres, boolean: "and", scope: undefined }];
804
808
  }
805
809
  }
806
- builder.wheres.push({
807
- type: "basic",
808
- column: orderColumn,
809
- operator: op,
810
- value: lastValue,
811
- boolean: "and",
812
- scope: undefined,
813
- });
810
+ builder.wheres.push({ type: "nested", column: "", query: this.compileCursorWheres(builder.orders, lastValues), boolean: "and", scope: undefined });
814
811
  }
815
812
  const items = await builder.get();
816
813
  if (items.length === 0)
@@ -821,11 +818,46 @@ export class Builder {
821
818
  if (items.length < chunkSize)
822
819
  break;
823
820
  const lastItem = items[items.length - 1];
824
- lastValue = lastItem && typeof lastItem === "object"
825
- ? lastItem[accessColumn]
821
+ lastValues = lastItem && typeof lastItem === "object"
822
+ ? builder.orders.map((order) => lastItem[this.getResultAccessColumn(order.column)])
826
823
  : undefined;
827
824
  }
828
825
  }
826
+ getResultAccessColumn(column) {
827
+ return column.includes(".") ? column.split(".").at(-1) : column;
828
+ }
829
+ compileCursorWheres(orders, values, index = 0) {
830
+ const order = orders[index];
831
+ const op = order.direction === "asc" ? ">" : "<";
832
+ const clauses = [{
833
+ type: "basic",
834
+ column: order.column,
835
+ operator: op,
836
+ value: values[index],
837
+ boolean: "and",
838
+ scope: undefined,
839
+ }];
840
+ if (index < orders.length - 1) {
841
+ clauses.push({
842
+ type: "nested",
843
+ column: "",
844
+ query: [
845
+ {
846
+ type: "basic",
847
+ column: order.column,
848
+ operator: "=",
849
+ value: values[index],
850
+ boolean: "and",
851
+ scope: undefined,
852
+ },
853
+ ...this.compileCursorWheres(orders, values, index + 1),
854
+ ],
855
+ boolean: "or",
856
+ scope: undefined,
857
+ });
858
+ }
859
+ return clauses;
860
+ }
829
861
  async *lazy(count = 1000) {
830
862
  let page = 1;
831
863
  while (true) {
@@ -844,7 +876,7 @@ export class Builder {
844
876
  const records = Array.isArray(data) ? data : [data];
845
877
  if (records.length === 0)
846
878
  return;
847
- const columns = Object.keys(records[0]);
879
+ const columns = this.getUniformColumns(records);
848
880
  const bindings = [];
849
881
  const values = records.map((record) => {
850
882
  return `(${columns.map((col) => {
@@ -863,7 +895,7 @@ export class Builder {
863
895
  const records = Array.isArray(data) ? data : [data];
864
896
  if (records.length === 0)
865
897
  return;
866
- const columns = Object.keys(records[0]);
898
+ const columns = this.getUniformColumns(records);
867
899
  const bindings = [];
868
900
  const values = records.map((record) => {
869
901
  return `(${columns.map((col) => {
@@ -878,7 +910,7 @@ export class Builder {
878
910
  const records = Array.isArray(data) ? data : [data];
879
911
  if (records.length === 0)
880
912
  return;
881
- const columns = Object.keys(records[0]);
913
+ const columns = this.getUniformColumns(records);
882
914
  const bindings = [];
883
915
  const values = records.map((record) => {
884
916
  return `(${columns.map((col) => {
@@ -891,6 +923,17 @@ export class Builder {
891
923
  const sql = this.grammar.compileUpsert(this.grammar.wrap(this.tableName), columns, values, uniqueCols, updateCols);
892
924
  return await this.connection.run(sql, bindings);
893
925
  }
926
+ getUniformColumns(records) {
927
+ const columns = Object.keys(records[0]);
928
+ const signature = [...columns].sort().join("\0");
929
+ for (let i = 1; i < records.length; i++) {
930
+ const recordSignature = Object.keys(records[i]).sort().join("\0");
931
+ if (recordSignature !== signature) {
932
+ throw new Error("Bulk insert records must have the same columns.");
933
+ }
934
+ }
935
+ return columns;
936
+ }
894
937
  async update(data) {
895
938
  this.bindings = [];
896
939
  this.parameterize = true;
@@ -10,7 +10,7 @@ export class MySqlGrammar extends Grammar {
10
10
  }
11
11
  if (value === "*")
12
12
  return value;
13
- return `\`${value}\``;
13
+ return `\`${value.replaceAll("`", "``")}\``;
14
14
  }
15
15
  placeholder(_index) {
16
16
  return "?";
@@ -10,7 +10,7 @@ export class PostgresGrammar extends Grammar {
10
10
  }
11
11
  if (value === "*")
12
12
  return value;
13
- return `"${value}"`;
13
+ return `"${value.replaceAll('"', '""')}"`;
14
14
  }
15
15
  placeholder(index) {
16
16
  return `$${index}`;
@@ -10,7 +10,7 @@ export class SQLiteGrammar extends Grammar {
10
10
  }
11
11
  if (value === "*")
12
12
  return value;
13
- return `"${value}"`;
13
+ return `"${value.replaceAll('"', '""')}"`;
14
14
  }
15
15
  placeholder(_index) {
16
16
  return "?";
@@ -135,48 +135,57 @@ export class Schema {
135
135
  const driver = connection.getDriverName();
136
136
  const schema = connection.getSchema() || "public";
137
137
  let sql;
138
+ let bindings = [];
138
139
  if (driver === "sqlite") {
139
- sql = `SELECT name FROM sqlite_master WHERE type='table' AND name='${table}'`;
140
+ sql = "SELECT name FROM sqlite_master WHERE type='table' AND name = ?";
141
+ bindings = [table];
140
142
  }
141
143
  else if (driver === "mysql") {
142
- sql = `SHOW TABLES LIKE '${table}'`;
144
+ sql = "SELECT table_name FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = ?";
145
+ bindings = [table];
143
146
  }
144
147
  else {
145
- sql = `SELECT * FROM information_schema.tables WHERE table_schema = '${schema}' AND table_name = '${table}'`;
148
+ sql = "SELECT table_name FROM information_schema.tables WHERE table_schema = $1 AND table_name = $2";
149
+ bindings = [schema, table];
146
150
  }
147
- const result = await connection.query(sql);
151
+ const result = await connection.query(sql, bindings);
148
152
  return result.length > 0;
149
153
  }
150
154
  static async hasColumn(table, column) {
151
155
  const connection = this.getConnection();
152
156
  const driver = connection.getDriverName();
153
157
  const schema = connection.getSchema() || "public";
158
+ const grammar = this.getGrammar();
154
159
  let sql;
160
+ let bindings = [];
155
161
  if (driver === "sqlite") {
156
- sql = `PRAGMA table_info(${table})`;
162
+ sql = `PRAGMA table_info(${grammar.wrap(table)})`;
157
163
  const result = await connection.query(sql);
158
164
  return result.some((row) => row.name === column);
159
165
  }
160
166
  else if (driver === "mysql") {
161
- sql = `SHOW COLUMNS FROM ${table} LIKE '${column}'`;
167
+ sql = "SELECT column_name FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = ? AND column_name = ?";
168
+ bindings = [table, column];
162
169
  }
163
170
  else {
164
- sql = `SELECT column_name FROM information_schema.columns WHERE table_schema = '${schema}' AND table_name = '${table}' AND column_name = '${column}'`;
171
+ sql = "SELECT column_name FROM information_schema.columns WHERE table_schema = $1 AND table_name = $2 AND column_name = $3";
172
+ bindings = [schema, table, column];
165
173
  }
166
- const result = await connection.query(sql);
174
+ const result = await connection.query(sql, bindings);
167
175
  return result.length > 0;
168
176
  }
169
177
  static async getColumn(table, column) {
170
178
  const connection = this.getConnection();
171
179
  const driver = connection.getDriverName();
172
180
  const schema = connection.getSchema() || "public";
181
+ const grammar = this.getGrammar();
173
182
  if (driver === "sqlite") {
174
- const rows = await connection.query(`PRAGMA table_info(${table})`);
183
+ const rows = await connection.query(`PRAGMA table_info(${grammar.wrap(table)})`);
175
184
  const row = rows.find((item) => item.name === column);
176
185
  return row ? { name: row.name, type: row.type, primary: row.pk > 0, autoIncrement: false } : null;
177
186
  }
178
187
  if (driver === "mysql") {
179
- const rows = await connection.query(`SHOW COLUMNS FROM ${table} LIKE '${column}'`);
188
+ const rows = await connection.query("SELECT column_name AS Field, column_type AS Type, column_key AS `Key`, extra AS Extra FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = ? AND column_name = ?", [table, column]);
180
189
  const row = rows[0];
181
190
  return row ? { name: row.Field, type: row.Type, primary: row.Key === "PRI", autoIncrement: String(row.Extra || "").toLowerCase().includes("auto_increment") } : null;
182
191
  }
@@ -189,9 +198,9 @@ export class Schema {
189
198
  LEFT JOIN information_schema.table_constraints tc
190
199
  ON kcu.table_schema = tc.table_schema
191
200
  AND kcu.constraint_name = tc.constraint_name
192
- WHERE c.table_schema = '${schema}'
193
- AND c.table_name = '${table}'
194
- AND c.column_name = '${column}'`);
201
+ WHERE c.table_schema = $1
202
+ AND c.table_name = $2
203
+ AND c.column_name = $3`, [schema, table, column]);
195
204
  const row = rows[0];
196
205
  return row ? { name: row.column_name, type: row.data_type, primary: !!row.primary_key, autoIncrement: false } : null;
197
206
  }
@@ -5,7 +5,7 @@ export class Grammar {
5
5
  return value.split(".").map((v) => this.wrap(v)).join(".");
6
6
  }
7
7
  const { prefix, suffix } = this.wrappers;
8
- return `${prefix}${value}${suffix}`;
8
+ return `${prefix}${value.replaceAll(suffix, `${suffix}${suffix}`)}${suffix}`;
9
9
  }
10
10
  wrapArray(values) {
11
11
  return values.map((v) => this.wrap(v));
@@ -29,7 +29,7 @@ export interface ForeignKeyDefinition {
29
29
  onUpdate?: string;
30
30
  }
31
31
  export interface WhereClause {
32
- type: "basic" | "in" | "null" | "raw" | "between" | "column" | "exists" | "like" | "regexp" | "fulltext" | "json_contains" | "json_length" | "date" | "all" | "any";
32
+ type: "basic" | "in" | "null" | "raw" | "nested" | "between" | "column" | "exists" | "like" | "regexp" | "fulltext" | "json_contains" | "json_length" | "date" | "all" | "any";
33
33
  column: string;
34
34
  columns?: string[];
35
35
  operator?: string;
@@ -38,6 +38,7 @@ export interface WhereClause {
38
38
  scope?: string;
39
39
  not?: boolean;
40
40
  dateType?: string;
41
+ query?: WhereClause[];
41
42
  }
42
43
  export interface OrderClause {
43
44
  column: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bunnykit/orm",
3
- "version": "0.1.21",
3
+ "version": "0.1.23",
4
4
  "description": "An Eloquent-inspired ORM for Bun's native SQL client supporting SQLite, MySQL, and PostgreSQL.",
5
5
  "license": "MIT",
6
6
  "packageManager": "bun@1.3.12",