1
0
mirror of https://github.com/vlang/v.git synced 2023-08-10 21:13:21 +03:00

vlib: move the mysql/sqlite/pg/mssql modules under vlib/db (#16820)

This commit is contained in:
yuyi
2023-01-13 23:02:32 +08:00
committed by GitHub
parent 2d8f160ef1
commit 64558df764
76 changed files with 3668 additions and 320 deletions

39
vlib/db/sqlite/README.md Normal file
View File

@@ -0,0 +1,39 @@
## Description
`sqlite` is a thin wrapper for [the SQLite library](https://sqlite.org/), which in turn is
"a C-language library that implements a small, fast, self-contained,
high-reliability, full-featured, SQL database engine."
# Install SQLite Dependency
Before you can use this module, you must first have the SQLite development
library installed on your system.
**Fedora 31**:
`sudo dnf -y install sqlite-devel`
**Ubuntu 20.04**:
`sudo apt install -y libsqlite3-dev`
**Windows**:
- Download the source zip from [SQLite Downloads](https://sqlite.org/download.html)
- Create a new `sqlite` subfolder inside `v/thirdparty`
- Extract the zip into that folder
# Performance Tips
When performing a large amount of database calls (i.e. INSERTS), significant
performance increase can be obtained by controlling the synchronization and journal modes.
For instance:
```v
import db.sqlite
db := sqlite.connect('foo.db') or { panic(err) }
db.synchronization_mode(sqlite.SyncMode.off)
db.journal_mode(sqlite.JournalMode.memory)
```

184
vlib/db/sqlite/orm.v Normal file
View File

@@ -0,0 +1,184 @@
module sqlite
import orm
import time
// @select is used internally by V's ORM for processing `SELECT ` queries
pub fn (db DB) @select(config orm.SelectConfig, data orm.QueryData, where orm.QueryData) ![][]orm.Primitive {
// 1. Create query and bind necessary data
query := orm.orm_select_gen(config, '`', true, '?', 1, where)
$if trace_sqlite ? {
eprintln('> @select query: "${query}"')
}
stmt := db.new_init_stmt(query)!
defer {
stmt.finalize()
}
mut c := 1
sqlite_stmt_binder(stmt, where, query, mut c)!
sqlite_stmt_binder(stmt, data, query, mut c)!
mut ret := [][]orm.Primitive{}
if config.is_count {
// 2. Get count of returned values & add it to ret array
step := stmt.step()
if step !in [sqlite_row, sqlite_ok, sqlite_done] {
return db.error_message(step, query)
}
count := stmt.sqlite_select_column(0, 8)!
ret << [count]
return ret
}
for {
// 2. Parse returned values
step := stmt.step()
if step == sqlite_done {
break
}
if step != sqlite_ok && step != sqlite_row {
break
}
mut row := []orm.Primitive{}
for i, typ in config.types {
primitive := stmt.sqlite_select_column(i, typ)!
row << primitive
}
ret << row
}
return ret
}
// sql stmt
// insert is used internally by V's ORM for processing `INSERT ` queries
pub fn (db DB) insert(table string, data orm.QueryData) ! {
query, converted_data := orm.orm_stmt_gen(table, '`', .insert, true, '?', 1, data,
orm.QueryData{})
sqlite_stmt_worker(db, query, converted_data, orm.QueryData{})!
}
// update is used internally by V's ORM for processing `UPDATE ` queries
pub fn (db DB) update(table string, data orm.QueryData, where orm.QueryData) ! {
query, _ := orm.orm_stmt_gen(table, '`', .update, true, '?', 1, data, where)
sqlite_stmt_worker(db, query, data, where)!
}
// delete is used internally by V's ORM for processing `DELETE ` queries
pub fn (db DB) delete(table string, where orm.QueryData) ! {
query, _ := orm.orm_stmt_gen(table, '`', .delete, true, '?', 1, orm.QueryData{}, where)
sqlite_stmt_worker(db, query, orm.QueryData{}, where)!
}
// last_id is used internally by V's ORM for post-processing `INSERT ` queries
pub fn (db DB) last_id() orm.Primitive {
query := 'SELECT last_insert_rowid();'
id := db.q_int(query)
return orm.Primitive(id)
}
// DDL (table creation/destroying etc)
// create is used internally by V's ORM for processing table creation queries (DDL)
pub fn (db DB) create(table string, fields []orm.TableField) ! {
query := orm.orm_table_gen(table, '`', true, 0, fields, sqlite_type_from_v, false) or {
return err
}
sqlite_stmt_worker(db, query, orm.QueryData{}, orm.QueryData{})!
}
// drop is used internally by V's ORM for processing table destroying queries (DDL)
pub fn (db DB) drop(table string) ! {
query := 'DROP TABLE `${table}`;'
sqlite_stmt_worker(db, query, orm.QueryData{}, orm.QueryData{})!
}
// helper
// Executes query and bind prepared statement data directly
fn sqlite_stmt_worker(db DB, query string, data orm.QueryData, where orm.QueryData) ! {
$if trace_sqlite ? {
eprintln('> sqlite_stmt_worker query: "${query}"')
}
stmt := db.new_init_stmt(query)!
defer {
stmt.finalize()
}
mut c := 1
sqlite_stmt_binder(stmt, data, query, mut c)!
sqlite_stmt_binder(stmt, where, query, mut c)!
stmt.orm_step(query)!
}
// Binds all values of d in the prepared statement
fn sqlite_stmt_binder(stmt Stmt, d orm.QueryData, query string, mut c &int) ! {
for data in d.data {
err := bind(stmt, c, data)
if err != 0 {
return stmt.db.error_message(err, query)
}
c++
}
}
// Universal bind function
fn bind(stmt Stmt, c &int, data orm.Primitive) int {
mut err := 0
match data {
i8, i16, int, u8, u16, u32, bool {
err = stmt.bind_int(c, int(data))
}
i64, u64 {
err = stmt.bind_i64(c, i64(data))
}
f32, f64 {
err = stmt.bind_f64(c, unsafe { *(&f64(&data)) })
}
string {
err = stmt.bind_text(c, data)
}
time.Time {
err = stmt.bind_int(c, int(data.unix))
}
orm.InfixType {
err = bind(stmt, c, data.right)
}
}
return err
}
// Selects column in result and converts it to an orm.Primitive
fn (stmt Stmt) sqlite_select_column(idx int, typ int) !orm.Primitive {
mut primitive := orm.Primitive(0)
if typ in orm.nums || typ == -1 {
primitive = stmt.get_int(idx)
} else if typ in orm.num64 {
primitive = stmt.get_i64(idx)
} else if typ in orm.float {
primitive = stmt.get_f64(idx)
} else if typ == orm.type_string {
primitive = stmt.get_text(idx).clone()
} else if typ == orm.time {
d := stmt.get_int(idx)
primitive = time.unix(d)
} else {
return error('Unknown type ${typ}')
}
return primitive
}
// Convert type int to sql type string
fn sqlite_type_from_v(typ int) !string {
return if typ in orm.nums || typ < 0 || typ in orm.num64 || typ == orm.time {
'INTEGER'
} else if typ in orm.float {
'REAL'
} else if typ == orm.type_string {
'TEXT'
} else {
error('Unknown type ${typ}')
}
}

View File

@@ -0,0 +1,118 @@
module sqlite
// Result represents Sqlite Result and Error Codes
// see https://www.sqlite.org/rescode.html
pub enum Result {
ok = 0
error = 1
internal = 2
perm = 3
abort = 4
busy = 5
locked = 6
nomem = 7
readonly = 8
interrupt = 9
ioerr = 10
corrupt = 11
notfound = 12
full = 13
cantopen = 14
protocol = 15
empty = 16
schema = 17
toobig = 18
constraint = 19
mismatch = 20
misuse = 21
nolfs = 22
auth = 23
format = 24
range = 25
notadb = 26
notice = 27
warning = 28
row = 100
done = 101
ok_load_permanently = 256
error_missing_collseq = 257
busy_recovery = 261
locked_sharedcache = 262
readonly_recovery = 264
ioerr_read = 266
corrupt_vtab = 267
cantopen_notempdir = 270
constraint_check = 275
notice_recover_wal = 283
warning_autoindex = 284
error_retry = 513
abort_rollback = 516
busy_snapshot = 517
locked_vtab = 518
readonly_cantlock = 520
ioerr_short_read = 522
corrupt_sequence = 523
cantopen_isdir = 526
constraint_commithook = 531
notice_recover_rollback = 539
error_snapshot = 769
busy_timeout = 773
readonly_rollback = 776
ioerr_write = 778
corrupt_index = 779
cantopen_fullpath = 782
constraint_foreignkey = 787
readonly_dbmoved = 1032
ioerr_fsync = 1034
cantopen_convpath = 1038
constraint_function = 1043
readonly_cantinit = 1288
ioerr_dir_fsync = 1290
cantopen_dirtywal = 1294
constraint_notnull = 1299
readonly_directory = 1544
ioerr_truncate = 1546
cantopen_symlink = 1550
constraint_primarykey = 1555
ioerr_fstat = 1802
constraint_trigger = 1811
ioerr_unlock = 2058
constraint_unique = 2067
ioerr_rdlock = 2314
constraint_vtab = 2323
ioerr_delete = 2570
constraint_rowid = 2579
ioerr_blocked = 2826
constraint_pinned = 2835
ioerr_nomem = 3082
ioerr_access = 3338
ioerr_checkreservedlock = 3594
ioerr_lock = 3850
ioerr_close = 4106
ioerr_dir_close = 4362
ioerr_shmopen = 4618
ioerr_shmsize = 4874
ioerr_shmlock = 5130
ioerr_shmmap = 5386
ioerr_seek = 5642
ioerr_delete_noent = 5898
ioerr_mmap = 6154
ioerr_gettemppath = 6410
ioerr_convpath = 6666
ioerr_vnode = 6922
ioerr_auth = 7178
ioerr_begin_atomic = 7434
ioerr_commit_atomic = 7690
ioerr_rollback_atomic = 7946
ioerr_data = 8202
}
// is_error checks if it is an error code.
pub fn (r Result) is_error() bool {
return r !in [.ok, .row, .done]
}
// is_error checks if `code` is an error code.
pub fn is_error(code int) bool {
return unsafe { Result(code).is_error() }
}

340
vlib/db/sqlite/sqlite.v Normal file
View File

@@ -0,0 +1,340 @@
module sqlite
$if freebsd || openbsd {
#flag -I/usr/local/include
#flag -L/usr/local/lib
}
$if windows {
#flag windows -I@VEXEROOT/thirdparty/sqlite
#flag windows -L@VEXEROOT/thirdparty/sqlite
#flag windows @VEXEROOT/thirdparty/sqlite/sqlite3.o
} $else {
#flag -lsqlite3
}
#include "sqlite3.h"
// https://www.sqlite.org/rescode.html
pub const (
sqlite_ok = 0
sqlite_error = 1
sqlite_row = 100
sqlite_done = 101
sqlite_cantopen = 14
sqlite_ioerr_read = 266
sqlite_ioerr_short_read = 522
sqlite_ioerr_write = 778
sqlite_ioerr_fsync = 1034
sqlite_ioerr_fstat = 1802
sqlite_ioerr_delete = 2570
sqlite_open_main_db = 0x00000100
sqlite_open_temp_db = 0x00000200
sqlite_open_transient_db = 0x00000400
sqlite_open_main_journal = 0x00000800
sqlite_open_temp_journal = 0x00001000
sqlite_open_subjournal = 0x00002000
sqlite_open_super_journal = 0x00004000
sqlite_open_wal = 0x00080000
)
pub enum SyncMode {
off
normal
full
}
pub enum JournalMode {
off
delete
truncate
persist
memory
}
struct C.sqlite3 {
}
struct C.sqlite3_stmt {
}
[heap]
pub struct Stmt {
stmt &C.sqlite3_stmt = unsafe { nil }
db &DB = unsafe { nil }
}
struct SQLError {
MessageError
}
//
[heap]
pub struct DB {
pub mut:
is_open bool
mut:
conn &C.sqlite3 = unsafe { nil }
}
// str returns a text representation of the DB
pub fn (db &DB) str() string {
return 'sqlite.DB{ conn: ' + ptr_str(db.conn) + ' }'
}
pub struct Row {
pub mut:
vals []string
}
//
fn C.sqlite3_open(&char, &&C.sqlite3) int
fn C.sqlite3_close(&C.sqlite3) int
fn C.sqlite3_busy_timeout(db &C.sqlite3, ms int) int
fn C.sqlite3_last_insert_rowid(&C.sqlite3) i64
//
fn C.sqlite3_prepare_v2(&C.sqlite3, &char, int, &&C.sqlite3_stmt, &&char) int
fn C.sqlite3_step(&C.sqlite3_stmt) int
fn C.sqlite3_finalize(&C.sqlite3_stmt) int
//
fn C.sqlite3_column_name(&C.sqlite3_stmt, int) &char
fn C.sqlite3_column_text(&C.sqlite3_stmt, int) &u8
fn C.sqlite3_column_int(&C.sqlite3_stmt, int) int
fn C.sqlite3_column_int64(&C.sqlite3_stmt, int) i64
fn C.sqlite3_column_double(&C.sqlite3_stmt, int) f64
fn C.sqlite3_column_count(&C.sqlite3_stmt) int
//
fn C.sqlite3_errstr(int) &char
fn C.sqlite3_errmsg(&C.sqlite3) &char
fn C.sqlite3_free(voidptr)
fn C.sqlite3_changes(&C.sqlite3) int
// connect Opens the connection with a database.
pub fn connect(path string) !DB {
db := &C.sqlite3(0)
code := C.sqlite3_open(&char(path.str), &db)
if code != 0 {
return &SQLError{
msg: unsafe { cstring_to_vstring(&char(C.sqlite3_errstr(code))) }
code: code
}
}
return DB{
conn: db
is_open: true
}
}
// close Closes the DB.
// TODO: For all functions, determine whether the connection is
// closed first, and determine what to do if it is
pub fn (mut db DB) close() !bool {
code := C.sqlite3_close(db.conn)
if code == 0 {
db.is_open = false
} else {
return &SQLError{
msg: unsafe { cstring_to_vstring(&char(C.sqlite3_errstr(code))) }
code: code
}
}
return true // successfully closed
}
// Only for V ORM
fn get_int_from_stmt(stmt &C.sqlite3_stmt) int {
x := C.sqlite3_step(stmt)
if x != C.SQLITE_OK && x != C.SQLITE_DONE {
C.puts(C.sqlite3_errstr(x))
}
res := C.sqlite3_column_int(stmt, 0)
C.sqlite3_finalize(stmt)
return res
}
// last_insert_rowid returns last inserted rowid
// https://www.sqlite.org/c3ref/last_insert_rowid.html
pub fn (db &DB) last_insert_rowid() i64 {
return C.sqlite3_last_insert_rowid(db.conn)
}
// get_affected_rows_count returns `sqlite changes()` meaning amount of rows affected by most recent sql query
pub fn (db &DB) get_affected_rows_count() int {
return C.sqlite3_changes(db.conn)
}
// q_int returns a single integer value, from the first column of the result of executing `query`
pub fn (db &DB) q_int(query string) int {
stmt := &C.sqlite3_stmt(0)
defer {
C.sqlite3_finalize(stmt)
}
C.sqlite3_prepare_v2(db.conn, &char(query.str), query.len, &stmt, 0)
C.sqlite3_step(stmt)
res := C.sqlite3_column_int(stmt, 0)
return res
}
// q_string returns a single string value, from the first column of the result of executing `query`
pub fn (db &DB) q_string(query string) string {
stmt := &C.sqlite3_stmt(0)
defer {
C.sqlite3_finalize(stmt)
}
C.sqlite3_prepare_v2(db.conn, &char(query.str), query.len, &stmt, 0)
C.sqlite3_step(stmt)
val := unsafe { &u8(C.sqlite3_column_text(stmt, 0)) }
return if val != &u8(0) { unsafe { tos_clone(val) } } else { '' }
}
// exec executes the query on the given `db`, and returns an array of all the results, alongside any result code.
// Result codes: https://www.sqlite.org/rescode.html
[manualfree]
pub fn (db &DB) exec(query string) ([]Row, int) {
stmt := &C.sqlite3_stmt(0)
defer {
C.sqlite3_finalize(stmt)
}
C.sqlite3_prepare_v2(db.conn, &char(query.str), query.len, &stmt, 0)
nr_cols := C.sqlite3_column_count(stmt)
mut res := 0
mut rows := []Row{}
for {
res = C.sqlite3_step(stmt)
// Result Code SQLITE_ROW; Another row is available
if res != 100 {
// C.puts(C.sqlite3_errstr(res))
break
}
mut row := Row{}
for i in 0 .. nr_cols {
val := unsafe { &u8(C.sqlite3_column_text(stmt, i)) }
if val == &u8(0) {
row.vals << ''
} else {
row.vals << unsafe { tos_clone(val) }
}
}
rows << row
}
return rows, res
}
// exec_one executes a query on the given `db`.
// It returns either the first row from the result, if the query was successful, or an error.
[manualfree]
pub fn (db &DB) exec_one(query string) !Row {
rows, code := db.exec(query)
defer {
unsafe { rows.free() }
}
if rows.len == 0 {
return &SQLError{
msg: 'No rows'
code: code
}
} else if code != 101 {
return &SQLError{
msg: unsafe { cstring_to_vstring(&char(C.sqlite3_errstr(code))) }
code: code
}
}
res := rows[0]
return res
}
// error_message returns a proper V error, given an integer error code received from SQLite, and a query string
[manualfree]
pub fn (db &DB) error_message(code int, query string) IError {
errmsg := unsafe { cstring_to_vstring(&char(C.sqlite3_errmsg(db.conn))) }
msg := '${errmsg} (${code}) (${query})'
unsafe { errmsg.free() }
return SQLError{
msg: msg
code: code
}
}
// exec_none executes a query, and returns the integer SQLite result code.
// Use it, in case you don't expect any row results, but still want a result code.
// e.g. for queries like these: `INSERT INTO ... VALUES (...)`
pub fn (db &DB) exec_none(query string) int {
stmt := &C.sqlite3_stmt(0)
C.sqlite3_prepare_v2(db.conn, &char(query.str), query.len, &stmt, 0)
code := C.sqlite3_step(stmt)
C.sqlite3_finalize(stmt)
return code
}
// TODO pub fn (db &DB) exec_param(query string, param string) []Row {
// create_table issues a "create table if not exists" command to the db.
// It creates the table named 'table_name', with columns generated from 'columns' array.
// The default columns type will be TEXT.
pub fn (db &DB) create_table(table_name string, columns []string) {
db.exec('create table if not exists ${table_name} (' + columns.join(',\n') + ')')
}
// busy_timeout sets a busy timeout in milliseconds.
// Sleeps for a specified amount of time when a table is locked. The handler
// will sleep multiple times until at least "ms" milliseconds of sleeping have accumulated.
// (see https://www.sqlite.org/c3ref/busy_timeout.html)
pub fn (db &DB) busy_timeout(ms int) int {
return C.sqlite3_busy_timeout(db.conn, ms)
}
// synchronization_mode sets disk synchronization mode, which controls how
// aggressively SQLite will write data to physical storage.
// .off: No syncs at all. (fastest)
// .normal: Sync after each sequence of critical disk operations.
// .full: Sync after each critical disk operation (slowest).
pub fn (db &DB) synchronization_mode(sync_mode SyncMode) {
if sync_mode == .off {
db.exec('pragma synchronous = OFF;')
} else if sync_mode == .full {
db.exec('pragma synchronous = FULL;')
} else {
db.exec('pragma synchronous = NORMAL;')
}
}
// journal_mode controls how the journal file is stored and processed.
// .off: No journal record is kept. (fastest)
// .memory: Journal record is held in memory, rather than on disk.
// .delete: At the conclusion of a transaction, journal file is deleted.
// .truncate: Journal file is truncated to a length of zero bytes.
// .persist: Journal file is left in place, but the header is overwritten to indicate journal is no longer valid.
pub fn (db &DB) journal_mode(journal_mode JournalMode) {
if journal_mode == .off {
db.exec('pragma journal_mode = OFF;')
} else if journal_mode == .delete {
db.exec('pragma journal_mode = DELETE;')
} else if journal_mode == .truncate {
db.exec('pragma journal_mode = TRUNCATE;')
} else if journal_mode == .persist {
db.exec('pragma journal_mode = PERSIST;')
} else if journal_mode == .memory {
db.exec('pragma journal_mode = MEMORY;')
} else {
db.exec('pragma journal_mode = MEMORY;')
}
}

View File

@@ -0,0 +1,228 @@
import orm
import db.sqlite
import time
struct TestCustomSqlType {
id int [primary; sql: serial]
custom string [sql_type: 'INTEGER']
custom1 string [sql_type: 'TEXT']
custom2 string [sql_type: 'REAL']
custom3 string [sql_type: 'NUMERIC']
custom4 string
custom5 int
custom6 time.Time
}
struct TestDefaultAtribute {
id string [primary; sql: serial]
name string
created_at string [default: 'CURRENT_TIME']
created_at1 string [default: 'CURRENT_DATE']
created_at2 string [default: 'CURRENT_TIMESTAMP']
}
struct EntityToTest {
id int [notnull; sql_type: 'INTEGER']
smth string [notnull; sql_type: 'TEXT']
}
fn test_sqlite_orm() {
mut db := sqlite.connect(':memory:') or { panic(err) }
defer {
db.close() or { panic(err) }
}
db.create('Test', [
orm.TableField{
name: 'id'
typ: typeof[int]().idx
attrs: [
StructAttribute{
name: 'primary'
},
StructAttribute{
name: 'sql'
has_arg: true
kind: .plain
arg: 'serial'
},
]
},
orm.TableField{
name: 'name'
typ: typeof[string]().idx
attrs: []
},
orm.TableField{
name: 'age'
typ: typeof[i64]().idx
},
]) or { panic(err) }
db.insert('Test', orm.QueryData{
fields: ['name', 'age']
data: [orm.string_to_primitive('Louis'), orm.i64_to_primitive(100)]
}) or { panic(err) }
res := db.@select(orm.SelectConfig{
table: 'Test'
has_where: true
fields: ['id', 'name', 'age']
types: [typeof[int]().idx, typeof[string]().idx, typeof[i64]().idx]
}, orm.QueryData{}, orm.QueryData{
fields: ['name', 'age']
data: [orm.Primitive('Louis'), i64(100)]
types: [typeof[string]().idx, typeof[i64]().idx]
is_and: [true, true]
kinds: [.eq, .eq]
}) or { panic(err) }
id := res[0][0]
name := res[0][1]
age := res[0][2]
assert id is int
if id is int {
assert id == 1
}
assert name is string
if name is string {
assert name == 'Louis'
}
assert age is i64
if age is i64 {
assert age == 100
}
/** test orm sql type
* - verify if all type create by attribute sql_type has created
*/
sql db {
create table TestCustomSqlType
}
mut result_custom_sql, mut exec_custom_code := db.exec('
pragma table_info(TestCustomSqlType);
')
assert exec_custom_code == 101
mut table_info_types_results := []string{}
information_schema_custom_sql := ['INTEGER', 'INTEGER', 'TEXT', 'REAL', 'NUMERIC', 'TEXT',
'INTEGER', 'INTEGER']
for data_type in result_custom_sql {
table_info_types_results << data_type.vals[2]
}
assert table_info_types_results == information_schema_custom_sql
sql db {
drop table TestCustomSqlType
}
/** test default attribute
*/
sql db {
create table TestDefaultAtribute
}
mut result_default_sql, mut code := db.exec('
pragma table_info(TestDefaultAtribute);
')
assert code == 101
mut information_schema_data_types_results := []string{}
information_schema_default_sql := ['', '', 'CURRENT_TIME', 'CURRENT_DATE', 'CURRENT_TIMESTAMP']
for data_type in result_default_sql {
information_schema_data_types_results << data_type.vals[4]
}
assert information_schema_data_types_results == information_schema_default_sql
test_default_atribute := TestDefaultAtribute{
name: 'Hitalo'
}
sql db {
insert test_default_atribute into TestDefaultAtribute
}
result_test_default_atribute := sql db {
select from TestDefaultAtribute limit 1
}
assert result_test_default_atribute.name == 'Hitalo'
assert test_default_atribute.created_at.len == 0
assert test_default_atribute.created_at1.len == 0
assert test_default_atribute.created_at2.len == 0
assert result_test_default_atribute.created_at.len == 8 // HH:MM:SS
assert result_test_default_atribute.created_at1.len == 10 // YYYY-MM-DD
assert result_test_default_atribute.created_at2.len == 19 // YYYY-MM-DD HH:MM:SS
sql db {
drop table TestDefaultAtribute
}
}
fn test_get_affected_rows_count() {
mut db := sqlite.connect(':memory:') or { panic(err) }
defer {
db.close() or { panic(err) }
}
db.exec('create table EntityToTest(
id integer not null constraint tbl_pk primary key,
smth integer
);')
fst := EntityToTest{
id: 1
smth: '1'
}
sql db {
insert fst into EntityToTest
} or { panic('first insert failed') }
assert db.get_affected_rows_count() == 1
snd := EntityToTest{
id: 1
smth: '2'
}
mut sndfailed := false
sql db {
insert snd into EntityToTest
} or { sndfailed = true }
assert db.get_affected_rows_count() == 0
assert sndfailed
all := sql db {
select from EntityToTest
}
assert 1 == all.len
sql db {
update EntityToTest set smth = '2' where id == 1
}
assert db.get_affected_rows_count() == 1
sql db {
update EntityToTest set smth = '2' where id == 2
}
assert db.get_affected_rows_count() == 0
sql db {
delete from EntityToTest where id == 2
}
assert db.get_affected_rows_count() == 0
sql db {
delete from EntityToTest where id == 1
}
assert db.get_affected_rows_count() == 1
}

View File

@@ -0,0 +1,92 @@
import db.sqlite
type Connection = sqlite.DB
struct User {
pub:
id int [primary; sql: serial]
name string
}
type Content = []u8 | string
struct Host {
pub:
db Connection
}
fn (back Host) get_users() []User {
return []
}
fn create_host(db Connection) Host {
sql db {
create table User
}
return Host{
db: db
}
}
fn test_sqlite() {
$if !linux {
return
}
mut db := sqlite.connect(':memory:') or { panic(err) }
assert db.is_open
db.exec('drop table if exists users')
db.exec("create table users (id integer primary key, name text default '');")
db.exec("insert into users (name) values ('Sam')")
assert db.last_insert_rowid() == 1
assert db.get_affected_rows_count() == 1
db.exec("insert into users (name) values ('Peter')")
assert db.last_insert_rowid() == 2
db.exec("insert into users (name) values ('Kate')")
assert db.last_insert_rowid() == 3
nr_users := db.q_int('select count(*) from users')
assert nr_users == 3
name := db.q_string('select name from users where id = 1')
assert name == 'Sam'
// this insert will be rejected due to duplicated id
db.exec("insert into users (id,name) values (1,'Sam')")
assert db.get_affected_rows_count() == 0
users, mut code := db.exec('select * from users')
assert users.len == 3
assert code == 101
code = db.exec_none('vacuum')
assert code == 101
user := db.exec_one('select * from users where id = 3') or { panic(err) }
println(user)
assert user.vals.len == 2
db.exec("update users set name='zzzz' where name='qqqq'")
assert db.get_affected_rows_count() == 0
db.exec("update users set name='Peter1' where name='Peter'")
assert db.get_affected_rows_count() == 1
db.exec("delete from users where name='qqqq'")
assert db.get_affected_rows_count() == 0
db.exec("delete from users where name='Sam'")
assert db.get_affected_rows_count() == 1
db.close() or { panic(err) }
assert !db.is_open
}
fn test_can_access_sqlite_result_consts() {
assert sqlite.sqlite_ok == 0
assert sqlite.sqlite_error == 1
// assert sqlite.misuse == 21
assert sqlite.sqlite_row == 100
assert sqlite.sqlite_done == 101
}
fn test_alias_db() {
create_host(sqlite.connect(':memory:')!)
assert true
}

View File

@@ -0,0 +1,284 @@
import db.sqlite
import rand
const (
max_file_name_len = 256
)
fn test_vfs_register() {
org_default_vfs := sqlite.get_default_vfs()?
assert org_default_vfs.zName != 0
vfs_name := 'sometest'
mut vfs_descr := &sqlite.Sqlite3_vfs{
zName: vfs_name.str
iVersion: 2
}
if _ := sqlite.get_vfs(vfs_name) {
panic('expected that vfs is not known')
}
vfs_descr.register_as_nondefault() or { panic('vfs register failed ${err}') }
sqlite.get_vfs(vfs_name)?
now_default_vfs := sqlite.get_default_vfs()?
assert now_default_vfs.zName == org_default_vfs.zName
vfs_descr.unregister() or { panic('vfs unregister failed ${err}') }
if _ := sqlite.get_vfs(vfs_name) {
panic('vfs supposedly unregistered yet somehow still foundable')
}
}
// minimal vfs based on example https://www.sqlite.org/src/doc/trunk/src/test_demovfs.c
fn test_verify_vfs_is_actually_used() {
wrapped := sqlite.get_default_vfs()?
vfs_name := 'sometest'
mut vfs_state := &ExampleVfsState{
log: []string{cap: 100}
}
mut vfs_descr := &sqlite.Sqlite3_vfs{
iVersion: 2
szOsFile: int(sizeof(ExampleVfsOpenedFile))
mxPathname: max_file_name_len
zName: vfs_name.str
pAppData: vfs_state
xOpen: example_vfs_open
xDelete: example_vfs_delete
xAccess: example_vfs_access
xFullPathname: example_vfs_fullpathname
xDlOpen: wrapped.xDlOpen
xDlError: wrapped.xDlError
xDlSym: wrapped.xDlSym
xDlClose: wrapped.xDlClose
xRandomness: wrapped.xRandomness
xSleep: wrapped.xSleep
xCurrentTime: wrapped.xCurrentTime
xGetLastError: example_vfs_getlasterror
xCurrentTimeInt64: wrapped.xCurrentTimeInt64
}
vfs_descr.register_as_nondefault()?
// normally this would be written to disk
mut db := sqlite.connect_full('foo.db', [.readwrite, .create], vfs_name)!
assert ['fullpathname from=foo.db to=foo.db}', 'open temp?=false name=foo.db', 'read file=foo.db'] == vfs_state.log
vfs_state.log.clear()
db.close()!
assert ['close file=foo.db'] == vfs_state.log
}
struct ExampleVfsState {
mut:
log []string
}
struct ExampleVfsOpenedFile {
mut:
base sqlite.Sqlite3_file
name string
vfs_state &ExampleVfsState
}
fn to_vfsstate(t &sqlite.Sqlite3_vfs) &ExampleVfsState {
unsafe {
p := t.pAppData
if p == 0 {
assert false, 'p should not be 0'
}
return &ExampleVfsState(p)
}
}
fn to_vfsopenedfile(t &sqlite.Sqlite3_file) &ExampleVfsOpenedFile {
unsafe {
if t == 0 {
assert false, 't should not be 0'
}
return &ExampleVfsOpenedFile(t)
}
}
fn example_vfs_fullpathname(vfs &sqlite.Sqlite3_vfs, input &char, size_of_output int, output &char) int {
println('fullpathname called')
mut vfs_state := to_vfsstate(vfs)
from := unsafe { cstring_to_vstring(input) }
unsafe {
vmemcpy(output, input, from.len)
output[from.len] = u8(0)
}
result := unsafe { cstring_to_vstring(output) }
vfs_state.log << 'fullpathname from=${from} to=${result}}'
return sqlite.sqlite_ok
}
fn example_vfs_access(vfs &sqlite.Sqlite3_vfs, zPath &char, flags int, pResOut &int) int {
println('access called')
mut vfs_state := &ExampleVfsState{}
unsafe {
assert 0 != vfs.pAppData
vfs_state = &ExampleVfsState(vfs.pAppData)
}
vfs_state.log << 'accessed'
return sqlite.sqlite_ok
}
fn example_vfs_open(vfs &sqlite.Sqlite3_vfs, file_name_or_null_for_tempfile &char, vfs_opened_file &sqlite.Sqlite3_file, in_flags int, out_flags &int) int {
println('open called')
mut is_temp := false
mut file_name := ''
unsafe {
if file_name_or_null_for_tempfile == nil {
is_temp = true
file_name = rand.uuid_v4()
} else {
file_name = cstring_to_vstring(file_name_or_null_for_tempfile)
}
}
mut vfs_state := to_vfsstate(vfs)
unsafe {
mut outp := to_vfsopenedfile(vfs_opened_file)
outp.base.pMethods = &sqlite.Sqlite3_io_methods{
iVersion: 1
xClose: example_vfsfile_close
xRead: example_vfsfile_read
xWrite: example_vfsfile_write
xTruncate: example_vfsfile_truncate
xSync: example_vfsfile_sync
xFileSize: example_vfsfile_size
xLock: example_vfsfile_lock
xUnlock: example_vfsfile_unlock
xCheckReservedLock: example_vfsfile_checkreservedlock
xFileControl: example_vfsfile_filecontrol
xSectorSize: example_vfsfile_sectorsize
xDeviceCharacteristics: example_vfsfile_devicecharacteristics
}
outp.name = file_name.clone()
outp.vfs_state = vfs_state
}
vfs_state.log << 'open temp?=${is_temp} name=${file_name}'
return sqlite.sqlite_ok
}
fn example_vfsfile_checkreservedlock(file &sqlite.Sqlite3_file, pResOut &int) int {
println('file checkreservedlock')
unsafe {
*pResOut = 0
}
return sqlite.sqlite_ok
}
fn example_vfsfile_filecontrol(file &sqlite.Sqlite3_file, op int, arg voidptr) int {
println('file filecontrol')
return 0
}
fn example_vfsfile_devicecharacteristics(file &sqlite.Sqlite3_file) int {
println('file devicecharacteristics')
return 0
}
fn example_vfsfile_size(file &sqlite.Sqlite3_file, result &i64) int {
println('file size')
return sqlite.sqlite_ok
}
fn example_vfsfile_read(file &sqlite.Sqlite3_file, output voidptr, amount int, offset i64) int {
println('file read')
assert amount > 0
mut vfsfile := to_vfsopenedfile(file)
vfsfile.vfs_state.log << 'read file=${vfsfile.name}'
unsafe {
C.memset(output, 0, amount)
}
return sqlite.sqlite_ioerr_short_read
}
fn example_vfsfile_truncate(file &sqlite.Sqlite3_file, size i64) int {
println('file truncate')
return sqlite.sqlite_ok
}
fn example_vfsfile_sectorsize(file &sqlite.Sqlite3_file) int {
println('file sectorsize')
return 0
}
fn example_vfsfile_sync(file &sqlite.Sqlite3_file, flags int) int {
println('file sync called')
return sqlite.sqlite_ok
}
fn example_vfsfile_lock(file &sqlite.Sqlite3_file, elock int) int {
println('file lock called')
return sqlite.sqlite_ok
}
fn example_vfsfile_unlock(file &sqlite.Sqlite3_file, elock int) int {
println('file unlock called')
return sqlite.sqlite_ok
}
fn example_vfsfile_write(file &sqlite.Sqlite3_file, buf voidptr, amount int, offset i64) int {
println('file write called')
return sqlite.sqlite_ok
}
fn example_vfsfile_close(file &sqlite.Sqlite3_file) int {
println('file close called')
mut vfsfile := to_vfsopenedfile(file)
vfsfile.vfs_state.log << 'close file=${vfsfile.name}'
return sqlite.sqlite_ok
}
fn example_vfs_delete(vfs &sqlite.Sqlite3_vfs, name &char, sync_dir int) int {
println('vfs delete called')
return sqlite.sqlite_ok
}
fn example_vfs_getlasterror(vfs &sqlite.Sqlite3_vfs, i int, o &char) int {
println('vfs getlasterror called')
unsafe {
*o = 0
}
return sqlite.sqlite_ok
}

78
vlib/db/sqlite/stmt.v Normal file
View File

@@ -0,0 +1,78 @@
module sqlite
fn C.sqlite3_bind_double(&C.sqlite3_stmt, int, f64) int
fn C.sqlite3_bind_int(&C.sqlite3_stmt, int, int) int
fn C.sqlite3_bind_int64(&C.sqlite3_stmt, int, i64) int
fn C.sqlite3_bind_text(&C.sqlite3_stmt, int, &char, int, voidptr) int
// Only for V ORM
fn (db &DB) init_stmt(query string) (&C.sqlite3_stmt, int) {
// println('init_stmt("$query")')
stmt := &C.sqlite3_stmt(0)
err := C.sqlite3_prepare_v2(db.conn, &char(query.str), query.len, &stmt, 0)
return stmt, err
}
fn (db &DB) new_init_stmt(query string) !Stmt {
stmt, err := db.init_stmt(query)
if err != sqlite_ok {
return db.error_message(err, query)
}
return Stmt{stmt, db}
}
fn (stmt &Stmt) bind_int(idx int, v int) int {
return C.sqlite3_bind_int(stmt.stmt, idx, v)
}
fn (stmt &Stmt) bind_i64(idx int, v i64) int {
return C.sqlite3_bind_int64(stmt.stmt, idx, v)
}
fn (stmt &Stmt) bind_f64(idx int, v f64) int {
return C.sqlite3_bind_double(stmt.stmt, idx, v)
}
fn (stmt &Stmt) bind_text(idx int, s string) int {
return C.sqlite3_bind_text(stmt.stmt, idx, voidptr(s.str), s.len, 0)
}
fn (stmt &Stmt) get_int(idx int) int {
return C.sqlite3_column_int(stmt.stmt, idx)
}
fn (stmt &Stmt) get_i64(idx int) i64 {
return C.sqlite3_column_int64(stmt.stmt, idx)
}
fn (stmt &Stmt) get_f64(idx int) f64 {
return C.sqlite3_column_double(stmt.stmt, idx)
}
fn (stmt &Stmt) get_text(idx int) string {
b := &char(C.sqlite3_column_text(stmt.stmt, idx))
if b == &char(0) {
return ''
}
return unsafe { b.vstring() }
}
fn (stmt &Stmt) get_count() int {
return C.sqlite3_column_count(stmt.stmt)
}
fn (stmt &Stmt) step() int {
return C.sqlite3_step(stmt.stmt)
}
fn (stmt &Stmt) orm_step(query string) ! {
res := stmt.step()
if res != sqlite_ok && res != sqlite_done && res != sqlite_row {
return stmt.db.error_message(res, query)
}
}
fn (stmt &Stmt) finalize() {
C.sqlite3_finalize(stmt.stmt)
}

View File

@@ -0,0 +1,171 @@
module sqlite
type Sig1 = fn (&C.sqlite3_file, &i64) int // https://github.com/vlang/v/issues/16291
type Sig2 = fn (&Sqlite3_file, &int) int // https://github.com/vlang/v/issues/16291
pub type Sqlite3_file = C.sqlite3_file
// https://www.sqlite.org/c3ref/file.html
struct C.sqlite3_file {
pub mut:
pMethods &C.sqlite3_io_methods // Methods for an open file
}
// https://www.sqlite.org/c3ref/io_methods.html
[heap]
struct C.sqlite3_io_methods {
mut:
// version 1 and later fields
iVersion int
xClose fn (&Sqlite3_file) int
xRead fn (&Sqlite3_file, voidptr, int, i64) int
xWrite fn (&Sqlite3_file, voidptr, int, i64) int
xTruncate fn (&Sqlite3_file, i64) int
xSync fn (&Sqlite3_file, int) int
xFileSize Sig1
xLock fn (&Sqlite3_file, int) int
xUnlock fn (&Sqlite3_file, int) int
xCheckReservedLock Sig2
xFileControl fn (&Sqlite3_file, int, voidptr) int
xSectorSize fn (&Sqlite3_file) int
xDeviceCharacteristics fn (&Sqlite3_file) int
// version 2 and later fields
xShmMap fn (&Sqlite3_file, int, int, int, &voidptr) int
xShmLock fn (&Sqlite3_file, int, int, int) int
xShmBarrier fn (&Sqlite3_file)
xShmUnmap fn (&Sqlite3_file, int) int
// version 3 and later fields
xFetch fn (&Sqlite3_file, i64, int, &voidptr) int
xUnfetch fn (&Sqlite3_file, i64, voidptr) int
}
pub type Sqlite3_io_methods = C.sqlite3_io_methods
// https://www.sqlite.org/c3ref/vfs.html
type Fn_sqlite3_syscall_ptr = fn ()
pub type Sqlite3_vfs = C.sqlite3_vfs
[heap]
pub struct C.sqlite3_vfs {
pub mut:
// version 1 and later fields
iVersion int // Structure version number (currently 3)
szOsFile int // Size of subclassed sqlite3_file
mxPathname int // Maximum file pathname length
pNext &Sqlite3_vfs // Next registered VFS
zName &char // Name of this virtual file system
pAppData voidptr // Pointer to application-specific data
xOpen fn (&Sqlite3_vfs, &char, &Sqlite3_file, int, &int) int
xDelete fn (&Sqlite3_vfs, &char, int) int
xAccess fn (&Sqlite3_vfs, &char, int, &int) int
xFullPathname fn (&Sqlite3_vfs, &char, int, &char) int
xDlOpen fn (&Sqlite3_vfs, &char) voidptr
xDlError fn (&Sqlite3_vfs, int, &char)
xDlSym fn (&Sqlite3_vfs, voidptr, &char) voidptr // to fn accepting void and returning
xDlClose fn (&Sqlite3_vfs, voidptr)
xRandomness fn (&Sqlite3_vfs, int, &char) int
xSleep fn (&Sqlite3_vfs, int) int
xCurrentTime fn (&Sqlite3_vfs, &f64) int
xGetLastError fn (&Sqlite3_vfs, int, &char) int
// version two and later only fields
xCurrentTimeInt64 fn (&Sqlite3_vfs, &i64) int
// version three and later only fields
xSetSystemCall fn (&Sqlite3_vfs, &char, Fn_sqlite3_syscall_ptr) int
xGetSystemCall fn (&Sqlite3_vfs, &char) Fn_sqlite3_syscall_ptr
xNextSystemCall fn (&Sqlite3_vfs, &char) &char
}
// https://www.sqlite.org/c3ref/vfs_find.html
fn C.sqlite3_vfs_find(&char) &C.sqlite3_vfs
fn C.sqlite3_vfs_register(&C.sqlite3_vfs, int) int
fn C.sqlite3_vfs_unregister(&C.sqlite3_vfs) int
// get_vfs Requests sqlite to return instance of VFS with given name.
// when such vfs is not known, `none` is returned
pub fn get_vfs(name string) ?&Sqlite3_vfs {
res := C.sqlite3_vfs_find(name.str)
unsafe {
if res == nil {
return none
} else {
return res
}
}
}
// get_default_vfs Asks sqlite for default VFS instance
pub fn get_default_vfs() ?&Sqlite3_vfs {
unsafe {
res := C.sqlite3_vfs_find(nil)
if res == nil {
return none
} else {
return res
}
}
}
// register_as_nondefault Asks sqlite to register VFS passed in receiver argument as the known VFS.
// more info about VFS: https://www.sqlite.org/c3ref/vfs.html
// 'not TODOs' to prevent corruption: https://sqlite.org/howtocorrupt.html
// example VFS: https://www.sqlite.org/src/doc/trunk/src/test_demovfs.c
pub fn (mut v Sqlite3_vfs) register_as_nondefault() ? {
res := C.sqlite3_vfs_register(v, 0)
return if sqlite_ok == res { none } else { error('sqlite3_vfs_register returned ${res}') }
}
// unregister Requests sqlite to stop using VFS as passed in receiver argument
pub fn (mut v Sqlite3_vfs) unregister() ? {
res := C.sqlite3_vfs_unregister(v)
return if sqlite_ok == res { none } else { error('sqlite3_vfs_unregister returned ${res}') }
}
// https://www.sqlite.org/c3ref/open.html
fn C.sqlite3_open_v2(&char, &&C.sqlite3, int, &char) int
// https://www.sqlite.org/c3ref/c_open_autoproxy.html
pub enum OpenModeFlag {
readonly = 0x00000001
readwrite = 0x00000002
create = 0x00000004
uri = 0x00000040
memory = 0x00000080
nomutex = 0x00008000
fullmutex = 0x00010000
sharedcache = 0x00020000
privatecache = 0x00040000
exrescode = 0x02000000
nofollow = 0x01000000
}
// connect_full Opens connection to sqlite database. It gives more control than `open`.
// Flags give control over readonly and create decisions. Specific VFS can be chosen.
pub fn connect_full(path string, mode_flags []OpenModeFlag, vfs_name string) !DB {
db := &C.sqlite3(0)
mut flags := 0
for flag in mode_flags {
flags = flags | int(flag)
}
code := C.sqlite3_open_v2(&char(path.str), &db, flags, vfs_name.str)
if code != 0 {
return &SQLError{
msg: unsafe { cstring_to_vstring(&char(C.sqlite3_errstr(code))) }
code: code
}
}
return DB{
conn: db
is_open: true
}
}