File: README.md

package info (click to toggle)
golang-github-pocketbase-dbx 1.11.0-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 336 kB
  • sloc: sql: 62; makefile: 3
file content (748 lines) | stat: -rw-r--r-- 27,298 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
dbx
[![Go Report Card](https://goreportcard.com/badge/github.com/pocketbase/dbx)](https://goreportcard.com/report/github.com/pocketbase/dbx)
[![GoDoc](https://godoc.org/github.com/pocketbase/dbx?status.svg)](https://pkg.go.dev/github.com/pocketbase/dbx)
================================================================================

> ⚠️ This is a maintained fork of [go-ozzo/ozzo-dbx](https://github.com/go-ozzo/ozzo-dbx) (see [#103](https://github.com/go-ozzo/ozzo-dbx/issues/103)).
>
> Currently, the changes are primarily related to better SQLite support and some other minor improvements, implementing [#99](https://github.com/go-ozzo/ozzo-dbx/pull/99), [#100](https://github.com/go-ozzo/ozzo-dbx/pull/100) and [#102](https://github.com/go-ozzo/ozzo-dbx/pull/102).


## Summary

- [Description](#description)
- [Requirements](#requirements)
- [Installation](#installation)
- [Supported Databases](#supported-databases)
- [Getting Started](#getting-started)
- [Connecting to Database](#connecting-to-database)
- [Executing Queries](#executing-queries)
- [Binding Parameters](#binding-parameters)
- [Building Queries](#building-queries)
	- [Building SELECT Queries](#building-select-queries)
	- [Building Query Conditions](#building-query-conditions)
	- [Building Data Manipulation Queries](#building-data-manipulation-queries)
	- [Building Schema Manipulation Queries](#building-schema-manipulation-queries)
- [CRUD Operations](#crud-operations)
	- [Create](#create)
	- [Read](#read)
	- [Update](#update)
	- [Delete](#delete)
	- [Null Handling](#null-handling)
- [Quoting Table and Column Names](#quoting-table-and-column-names)
- [Using Transactions](#using-transactions)
- [Logging Executed SQL Statements](#logging-executed-sql-statements)
- [Supporting New Databases](#supporting-new-databases)


## Description

`dbx` is a Go package that enhances the standard `database/sql` package by providing powerful data retrieval methods
as well as DB-agnostic query building capabilities. `dbx` is not an ORM. It has the following features:

* Populating data into structs and NullString maps
* Named parameter binding
* DB-agnostic query building methods, including SELECT queries, data manipulation queries, and schema manipulation queries
* Inserting, updating, and deleting model structs
* Powerful query condition building
* Open architecture allowing addition of new database support or customization of existing support
* Logging executed SQL statements
* Supporting major relational databases

For an example on how this library is used in an application, please refer to [go-rest-api](https://github.com/qiangxue/go-rest-api) which is a starter kit for building RESTful APIs in Go.

## Requirements

Go 1.13 or above.

## Installation

Run the following command to install the package:

```
go get github.com/pocketbase/dbx
```

In addition, install the specific DB driver package for the kind of database to be used. Please refer to
[SQL database drivers](https://github.com/golang/go/wiki/SQLDrivers) for a complete list. For example, if you are
using MySQL, you may install the following package:

```sh
go get github.com/go-sql-driver/mysql
```

and import it in your main code like the following:

```go
import _ "github.com/go-sql-driver/mysql"
```

## Supported Databases

The following databases are fully supported out of box:

* SQLite
* MySQL
* PostgreSQL
* MS SQL Server (2012 or above)
* Oracle

For other databases, the query building feature may not work as expected. You can create a custom builder to
solve the problem. Please see the last section for more details.

## Getting Started

The following code snippet shows how you can use this package in order to access data from a MySQL database.

```go
package main

import (
	"github.com/pocketbase/dbx"
	_ "github.com/go-sql-driver/mysql"
)

func main() {
	db, _ := dbx.Open("mysql", "user:pass@/example")

	// create a new query
	q := db.NewQuery("SELECT id, name FROM users LIMIT 10")

	// fetch all rows into a struct array
	var users []struct {
		ID, Name string
	}
	q.All(&users)

	// fetch a single row into a struct
	var user struct {
		ID, Name string
	}
	q.One(&user)

	// fetch a single row into a string map
	data := dbx.NullStringMap{}
	q.One(data)

	// fetch row by row
	rows2, _ := q.Rows()
	for rows2.Next() {
		rows2.ScanStruct(&user)
		// rows.ScanMap(data)
		// rows.Scan(&id, &name)
	}
}
```

And the following example shows how to use the query building capability of this package.

```go
package main

import (
	"github.com/pocketbase/dbx"
	_ "github.com/go-sql-driver/mysql"
)

func main() {
	db, _ := dbx.Open("mysql", "user:pass@/example")

	// build a SELECT query
	//   SELECT `id`, `name` FROM `users` WHERE `name` LIKE '%Charles%' ORDER BY `id`
	q := db.Select("id", "name").
		From("users").
		Where(dbx.Like("name", "Charles")).
		OrderBy("id")

	// fetch all rows into a struct array
	var users []struct {
		ID, Name string
	}
	q.All(&users)

	// build an INSERT query
	//   INSERT INTO `users` (`name`) VALUES ('James')
	db.Insert("users", dbx.Params{
		"name": "James",
	}).Execute()
}
```

## Connecting to Database

To connect to a database, call `dbx.Open()` in the same way as you would do with the `Open()` method in `database/sql`.

```go
db, err := dbx.Open("mysql", "user:pass@hostname/db_name")
```

The method returns a `dbx.DB` instance which can be used to create and execute DB queries. Note that the method 
does not really establish a connection until a query is made using the returned `dbx.DB` instance. It also
does not check the correctness of the data source name either. Call `dbx.MustOpen()` to make sure the data 
source name is correct.

## Executing Queries

To execute a SQL statement, first create a `dbx.Query` instance by calling `DB.NewQuery()` with the SQL statement
to be executed. And then call `Query.Execute()` to execute the query if the query is not meant to retrieving data.
For example,

```go
q := db.NewQuery("UPDATE users SET status=1 WHERE id=100")
result, err := q.Execute()
```

If the SQL statement does retrieve data (e.g. a SELECT statement), one of the following methods should be called, 
which will execute the query and populate the result into the specified variable(s).

* `Query.All()`: populate all rows of the result into a slice of structs or `NullString` maps.
* `Query.One()`: populate the first row of the result into a struct or a `NullString` map.
* `Query.Column()`: populate the first column of the result into a slice.
* `Query.Row()`: populate the first row of the result into a list of variables, one for each returning column.
* `Query.Rows()`: returns a `dbx.Rows` instance to allow retrieving data row by row.

For example,

```go
type User struct {
	ID   int
	Name string
}

var (
	users []User
	user  User

	row dbx.NullStringMap

	id   int
	name string

	err error
)

q := db.NewQuery("SELECT id, name FROM users LIMIT 10")

// populate all rows into a User slice
err = q.All(&users)
fmt.Println(users[0].ID, users[0].Name)

// populate the first row into a User struct
err = q.One(&user)
fmt.Println(user.ID, user.Name)

// populate the first row into a NullString map
err = q.One(&row)
fmt.Println(row["id"], row["name"])

var ids []int
err = q.Column(&ids)
fmt.Println(ids)

// populate the first row into id and name
err = q.Row(&id, &name)

// populate data row by row
rows, _ := q.Rows()
for rows.Next() {
	_ = rows.ScanMap(&row)
}
```

When populating a struct, the following rules are used to determine which columns should go into which struct fields:

* Only exported struct fields can be populated.
* A field receives data if its name is mapped to a column according to the field mapping function `Query.FieldMapper`.
  The default field mapping function separates words in a field name by underscores and turns them into lower case.
  For example, a field name `FirstName` will be mapped to the column name `first_name`, and `MyID` to `my_id`.
* If a field has a `db` tag, the tag value will be used as the corresponding column name. If the `db` tag is a dash `-`,
  it means the field should NOT be populated.
* For anonymous fields that are of struct type, they will be expanded and their component fields will be populated
  according to the rules described above.
* For named fields that are of struct type, they will also be expanded. But their component fields will be prefixed
  with the struct names when being populated.
  
An exception to the above struct expansion is that when a struct type implements `sql.Scanner` or when it is `time.Time`.
In this case, the field will be populated as a whole by the DB driver. Also, if a field is a pointer to some type,
the field will be allocated memory and populated with the query result if it is not null. 

The following example shows how fields are populated according to the rules above:

```go
type User struct {
	id     int
	Type   int    `db:"-"`
	MyName string `db:"name"`
	Profile
	Address Address `db:"addr"`
}

type Profile struct {
	Age int
}

type Address struct {
	City string
}
```

* `User.id`: not populated because the field is not exported;
* `User.Type`: not populated because the `db` tag is `-`;
* `User.MyName`: to be populated from the `name` column, according to the `db` tag;
* `Profile.Age`: to be populated from the `age` column, since `Profile` is an anonymous field;
* `Address.City`: to be populated from the `addr.city` column, since `Address` is a named field of struct type
  and its fields will be prefixed with `addr.` according to the `db` tag.

Note that if a column in the result does not have a corresponding struct field, it will be ignored. Similarly,
if a struct field does not have a corresponding column in the result, it will not be populated.

## Binding Parameters

A SQL statement is usually parameterized with dynamic values. For example, you may want to select the user record
according to the user ID received from the client. Parameter binding should be used in this case, and it is almost
always preferred to prevent from SQL injection attacks. Unlike `database/sql` which does anonymous parameter binding, 
`dbx` uses named parameter binding. *Anonymous parameter binding is not supported*, as it will mess up with named
parameters. For example,

```go
q := db.NewQuery("SELECT id, name FROM users WHERE id={:id}")
q.Bind(dbx.Params{"id": 100})
err := q.One(&user)
```

The above example will select the user record whose `id` is 100. The method `Query.Bind()` binds a set
of named parameters to a SQL statement which contains parameter placeholders in the format of `{:ParamName}`.

If a SQL statement needs to be executed multiple times with different parameter values, it may be prepared
to improve the performance. For example,

```go
q := db.NewQuery("SELECT id, name FROM users WHERE id={:id}")
q.Prepare()
defer q.Close()

q.Bind(dbx.Params{"id": 100})
err := q.One(&user)

q.Bind(dbx.Params{"id": 200})
err = q.One(&user)

// ...
```


## Cancelable Queries

Queries are cancelable when they are used with `context.Context`. In particular, by calling `Query.WithContext()` you
can associate a context with a query and use the context to cancel the query while it is running. For example,

```go
q := db.NewQuery("SELECT id, name FROM users")
err := q.WithContext(ctx).All(&users)
```


## Building Queries

Instead of writing plain SQLs, `dbx` allows you to build SQLs programmatically, which often leads to cleaner,
more secure, and DB-agnostic code. You can build three types of queries: the SELECT queries, the data manipulation
queries, and the schema manipulation queries.

### Building SELECT Queries

Building a SELECT query starts by calling `DB.Select()`. You can build different clauses of a SELECT query using
the corresponding query building methods. For example,

```go
db, _ := dbx.Open("mysql", "user:pass@/example")
err := db.Select("id", "name").
	From("users").
	Where(dbx.HashExp{"id": 100}).
	One(&user)
```

The above code will generate and execute the following SQL statement:

```sql
SELECT `id`, `name` FROM `users` WHERE `id`={:p0} 
```

Notice how the table and column names are properly quoted according to the currently using database type.
And parameter binding is used to populate the value of `p0` in the `WHERE` clause.

Every SQL keyword has a corresponding query building method. For example, `SELECT` corresponds to `Select()`,
`FROM` corresponds to `From()`, `WHERE` corresponds to `Where()`, and so on. You can chain these method calls
together, just like you would do when writing a plain SQL. Each of these methods returns the query instance
(of type `dbx.SelectQuery`) that is being built. Once you finish building a query, you may call methods such as
`One()`, `All()` to execute the query and populate data into variables. You may also explicitly call `Build()`
to build the query and turn it into a `dbx.Query` instance which may allow you to get the SQL statement and do
other interesting work.


### Building Query Conditions

`dbx` supports very flexible and powerful query condition building which can be used to build SQL clauses
such as `WHERE`, `HAVING`, etc. For example,

```go
// id=100
dbx.NewExp("id={:id}", dbx.Params{"id": 100})

// id=100 AND status=1
dbx.HashExp{"id": 100, "status": 1}

// status=1 OR age>30
dbx.Or(dbx.HashExp{"status": 1}, dbx.NewExp("age>30"))

// name LIKE '%admin%' AND name LIKE '%example%'
dbx.Like("name", "admin", "example")
```

When building a query condition expression, its parameter values will be populated using parameter binding, which
prevents SQL injection from happening. Also if an expression involves column names, they will be properly quoted.
The following condition building functions are available:

* `dbx.NewExp()`: creating a condition using the given expression string and binding parameters. For example,
`dbx.NewExp("id={:id}", dbx.Params{"id":100})` would create the expression `id=100`.
* `dbx.HashExp`: a map type that represents name-value pairs concatenated by `AND` operators. For example,
`dbx.HashExp{"id":100, "status":1}` would create `id=100 AND status=1`.
* `dbx.Not()`: creating a `NOT` expression by prepending `NOT` to the given expression.
* `dbx.And()`: creating an `AND` expression by concatenating the given expressions with the `AND` operators.
* `dbx.Or()`: creating an `OR` expression by concatenating the given expressions with the `OR` operators.
* `dbx.In()`: creating an `IN` expression for the specified column and the range of values.
For example, `dbx.In("age", 30, 40, 50)` would create the expression `age IN (30, 40, 50)`.
Note that if the value range is empty, it will generate an expression representing a false value.
* `dbx.NotIn()`: creating an `NOT IN` expression. This is very similar to `dbx.In()`. 
* `dbx.Like()`: creating a `LIKE` expression for the specified column and the range of values. For example, 
`dbx.Like("title", "golang", "framework")` would create the expression `title LIKE "%golang%" AND title LIKE "%framework%"`.
You can further customize a LIKE expression by calling `Escape()` and/or `Match()` functions of the resulting expression.
Note that if the value range is empty, it will generate an empty expression. 
* `dbx.NotLike()`: creating a `NOT LIKE` expression. This is very similar to `dbx.Like()`.
* `dbx.OrLike()`: creating a `LIKE` expression but concatenating different `LIKE` sub-expressions using `OR` instead of `AND`.
* `dbx.OrNotLike()`: creating a `NOT LIKE` expression and concatenating different `NOT LIKE` sub-expressions using `OR` instead of `AND`.
* `dbx.Exists()`: creating an `EXISTS` expression by prepending `EXISTS` to the given expression.
* `dbx.NotExists()`: creating a `NOT EXISTS` expression by prepending `NOT EXISTS` to the given expression.
* `dbx.Between()`: creating a `BETWEEN` expression. For example, `dbx.Between("age", 30, 40)` would create the 
expression `age BETWEEN 30 AND 40`.
* `dbx.NotBetween()`: creating a `NOT BETWEEN` expression. For example

You may also create other convenient functions to help building query conditions, as long as the functions return
an object implementing the `dbx.Expression` interface.
 

### Building Data Manipulation Queries

Data manipulation queries are those changing the data in the database, such as INSERT, UPDATE, DELETE statements.
Such queries can be built by calling the corresponding methods of `DB`. For example,

```go
db, _ := dbx.Open("mysql", "user:pass@/example")

// INSERT INTO `users` (`name`, `email`) VALUES ({:p0}, {:p1})
err := db.Insert("users", dbx.Params{
	"name": "James",
	"email": "james@example.com",
}).Execute()

// UPDATE `users` SET `status`={:p0} WHERE `id`={:p1}
err = db.Update("users", dbx.Params{"status": 1}, dbx.HashExp{"id": 100}).Execute()

// DELETE FROM `users` WHERE `status`={:p0}
err = db.Delete("users", dbx.HashExp{"status": 2}).Execute()
```

When building data manipulation queries, remember to call `Execute()` at the end to execute the queries.

### Building Schema Manipulation Queries

Schema manipulation queries are those changing the database schema, such as creating a new table, adding a new column.
These queries can be built by calling the corresponding methods of `DB`. For example,

```go
db, _ := dbx.Open("mysql", "user:pass@/example")

// CREATE TABLE `users` (`id` int primary key, `name` varchar(255))
q := db.CreateTable("users", map[string]string{
	"id": "int primary key",
	"name": "varchar(255)",
})
err := q.Execute()
```

## CRUD Operations

Although `dbx` is not an ORM, it does provide a very convenient way to do typical CRUD (Create, Read, Update, Delete)
operations without the need of writing plain SQL statements.

To use the CRUD feature, first define a struct type for a table. By default, a struct is associated with a table
whose name is the snake case version of the struct type name. For example, a struct named `MyCustomer`
corresponds to the table name `my_customer`. You may explicitly specify the table name for a struct by implementing
the `dbx.TableModel` interface. For example,

```go
type MyCustomer struct{}

func (c MyCustomer) TableName() string {
	return "customer"
}
```

Note that the `TableName` method should be defined with a value receiver instead of a pointer receiver. 

If the struct has a field named `ID` or `Id`, by default the field will be treated as the primary key field.
If you want to use a different field as the primary key, tag it with `db:"pk"`. You may tag multiple fields
for composite primary keys. Note that if you also want to explicitly specify the column name for a primary key field,
you should use the tag format `db:"pk,col_name"`.

You can give a common prefix or suffix to your table names by defining your own table name mapping via 
`DB.TableMapFunc`. For example, the following code prefixes `tbl_` to all table names. 

```go
db.TableMapper = func(a interface{}) string {
    return "tbl_" + GetTableName(a)
}
```

### Create

To create (insert) a new row using a model, call the `ModelQuery.Insert()` method. For example,

```go
type Customer struct {
	ID     int
	Name   string
	Email  string
	Status int
}

db, _ := dbx.Open("mysql", "user:pass@/example")

customer := Customer{
	Name: "example",
	Email: "test@example.com",
}
// INSERT INTO customer (name, email, status) VALUES ('example', 'test@example.com', 0)
err := db.Model(&customer).Insert()
```

This will insert a row using the values from *all* public fields (except the primary key field if it is empty) in the struct.
If a primary key field is zero (a integer zero or a nil pointer), it is assumed to be auto-incremental and 
will be automatically filled with the last insertion ID after a successful insertion.

You can explicitly specify the fields that should be inserted by passing the list of the field names to the `Insert()` method.
You can also exclude certain fields from being inserted by calling `Exclude()` before calling `Insert()`. For example,

```go
db, _ := dbx.Open("mysql", "user:pass@/example")

// insert only Name and Email fields
err := db.Model(&customer).Insert("Name", "Email")
// insert all public fields except Status
err = db.Model(&customer).Exclude("Status").Insert()
// insert only Name
err = db.Model(&customer).Exclude("Status").Insert("Name", "Status")
```

### Read

To read a model by a given primary key value, call `SelectQuery.Model()`.

```go
db, _ := dbx.Open("mysql", "user:pass@/example")

var customer Customer
// SELECT * FROM customer WHERE id=100
err := db.Select().Model(100, &customer)

// SELECT name, email FROM customer WHERE status=1 AND id=100
err = db.Select("name", "email").Where(dbx.HashExp{"status": 1}).Model(100, &customer)
```

Note that `SelectQuery.Model()` does not support composite primary keys. You should use `SelectQuery.One()` in this case.
For example,

```go
db, _ := dbx.Open("mysql", "user:pass@/example")

var orderItem OrderItem

// SELECT * FROM order_item WHERE order_id=100 AND item_id=20
err := db.Select().Where(dbx.HashExp{"order_id": 100, "item_id": 20}).One(&orderItem)
```

In the above queries, we do not call `From()` to specify which table to select data from. This is because the select
query automatically sets the table according to the model struct being populated. If the struct implements `TableModel`,
the value returned by its `TableName()` method will be used as the table name. Otherwise, the snake case version
of the struct type name will be the table name.

You may also call `SelectQuery.All()` to read a list of model structs. Similarly, you do not need to call `From()`
if the table name can be inferred from the model structs.


### Update

To update a model, call the `ModelQuery.Update()` method. Like `Insert()`, by default, the `Update()` method will
update *all* public fields except primary key fields of the model. You can explicitly specify which fields can
be updated and which cannot in the same way as described for the `Insert()` method. For example,

```go
db, _ := dbx.Open("mysql", "user:pass@/example")

// update all public fields of customer
err := db.Model(&customer).Update()
// update only Status
err = db.Model(&customer).Update("Status")
// update all public fields except Status
err = db.Model(&customer).Exclude("Status").Update()
```

Note that the `Update()` method assumes that the primary keys are immutable. It uses the primary key value of the model
to look for the row that should be updated. An error will be returned if a model does not have a primary key.


### Delete

To delete a model, call the `ModelQuery.Delete()` method. The method deletes the row using the primary key value
specified by the model. If the model does not have a primary key, an error will be returned. For example,

```go
db, _ := dbx.Open("mysql", "user:pass@/example")

err := db.Model(&customer).Delete()
```

### Null Handling

To represent a nullable database value, you can use a pointer type. If the pointer is nil, it means the corresponding 
database value is null. 

Another option to represent a database null is to use `sql.NullXyz` types. For example, if a string column is nullable,
you may use `sql.NullString`. The `NullString.Valid` field indicates whether the value is a null or not, and 
`NullString.String` returns the string value when it is not null. Because `sql.NulLXyz` types do not handle JSON 
marshalling, you may use the [null package](https://github.com/guregu/null), instead. 

Below is an example of handling nulls:

```go
type Customer struct {
	ID        int
	Email     string
	FirstName *string        // use pointer to represent null
	LastName  sql.NullString // use sql.NullString to represent null
}
```

## Quoting Table and Column Names

Databases vary in quoting table and column names. To allow writing DB-agnostic SQLs, `dbx` introduces a special
syntax in quoting table and column names. A word enclosed within `{{` and `}}` is treated as a table name and will
be quoted according to the particular DB driver. Similarly, a word enclosed within `[[` and `]]` is treated as a 
column name and will be quoted accordingly as well. For example, when working with a MySQL database, the following
query will be properly quoted:

```go
// SELECT * FROM `users` WHERE `status`=1
q := db.NewQuery("SELECT * FROM {{users}} WHERE [[status]]=1")
```

Note that if a table or column name contains a prefix, it will still be properly quoted. For example, `{{public.users}}`
will be quoted as `"public"."users"` for PostgreSQL.

## Using Transactions

You can use all aforementioned query execution and building methods with transaction. For example,

```go
db, _ := dbx.Open("mysql", "user:pass@/example")

tx, _ := db.Begin()

_, err1 := tx.Insert("users", dbx.Params{
	"name": "user1",
}).Execute()
_, err2 := tx.Insert("users", dbx.Params{
	"name": "user2",
}).Execute()

if err1 == nil && err2 == nil {
	tx.Commit()
} else {
	tx.Rollback()
}
```

You may use `DB.Transactional()` to simplify your transactional code without explicitly committing or rolling back
transactions. The method will start a transaction and automatically roll back the transaction if the callback
returns an error. Otherwise it will
automatically commit the transaction.


```go
db, _ := dbx.Open("mysql", "user:pass@/example")

err := db.Transactional(func(tx *dbx.Tx) error {
	var err error
	_, err = tx.Insert("users", dbx.Params{
		"name": "user1",
	}).Execute()
	if err != nil {
		return err
	}
	_, err = tx.Insert("users", dbx.Params{
		"name": "user2",
	}).Execute()
	return err
})

fmt.Println(err)
```

## Logging Executed SQL Statements

You can log and instrument DB queries by installing loggers with a DB connection. There are three kinds of loggers you
can install:
* `DB.LogFunc`: this is called each time when a SQL statement is queried or executed. The function signature is the
  same as that of `fmt.Printf`, which makes it very easy to use. 
* `DB.QueryLogFunc`: this is called each time when querying with a SQL statement.
* `DB.ExecLogFunc`: this is called when executing a SQL statement.
 
The following example shows how you can make use of these loggers.

```go
package main

import (
	"context"
	"database/sql"
	"log"
	"time"

	"github.com/pocketbase/dbx"
)

func main() {
	db, _ := dbx.Open("mysql", "user:pass@/example")

	// simple logging
	db.LogFunc = log.Printf

	// or you can use the following more flexible logging
	db.QueryLogFunc = func(ctx context.Context, t time.Duration, sql string, rows *sql.Rows, err error) {
		log.Printf("[%.2fms] Query SQL: %v", float64(t.Milliseconds()), sql)
	}
	db.ExecLogFunc = func(ctx context.Context, t time.Duration, sql string, result sql.Result, err error) {
		log.Printf("[%.2fms] Execute SQL: %v", float64(t.Milliseconds()), sql)
	}
	// ...
}
``` 

## Supporting New Databases

While `dbx` provides out-of-box query building support for most major relational databases, its open architecture
allows you to add support for new databases. The effort of adding support for a new database involves:

* Create a struct that implements the `QueryBuilder` interface. You may use `BaseQueryBuilder` directly or extend it
  via composition.
* Create a struct that implements the `Builder` interface. You may extend `BaseBuilder` via composition.
* Write an `init()` function to register the new builder in `dbx.BuilderFuncMap`.