Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

sqlite: Functions thats returns the values resulting from a 'SELECT' query and name of column - Feature ISSUE #20565 #20650

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
102 changes: 102 additions & 0 deletions vlib/db/sqlite/sqlite.c.v
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
module sqlite

import regex { regex_opt }

$if freebsd || openbsd {
#flag -I/usr/local/include
#flag -L/usr/local/lib
Expand Down Expand Up @@ -85,6 +87,11 @@ pub mut:
vals []string
}

pub struct QuerySet {
pub mut:
vals map[string]string
}

//
fn C.sqlite3_open(&char, &&C.sqlite3) int

Expand Down Expand Up @@ -248,6 +255,101 @@ pub fn (db &DB) exec(query string) ![]Row {
return rows
}

// get_queryset returns the values resulting from a 'SELECT' query and the name of each column. If an alias is provided either through the 'as' command or not, the returned value becomes the alias.
@[manualfree]
pub fn (db &DB) get_queryset(query string) ![]QuerySet {
spytheman marked this conversation as resolved.
Show resolved Hide resolved
query_lower := query.to_lower()

// 'select' syntax verified on: https://www.sqlite.org/lang_select.html and
// https://www.sqlite.org/syntax/join-clause.html
mut select_header := regex_opt(r'select((\s)+(all)|(distinct)(\s)+)|(\s)+')!
mut from := regex_opt(r'(\s)+from(\s)+')!
Comment on lines +265 to +266
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hm, why is regex matching needed at all?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I use the string query itself to obtain the names of the columns if they have been specified, as well as the name of the table! Since there are several ways to initiate the 'SELECT' command doc, I constructed a regex based on the syntax to facilitate obtaining the column names and the table name!


// or do not include 'FROM', just like 'SELECT 1 + 1'
if query_lower.count('select') == 1 && query_lower.contains('from') {
// The execution of this function indicates that the passed
// query is syntactically correct, which is why no additional verification
rows := db.exec(query)!

defer {
unsafe { rows.free() }
}

if rows.len == 0 {
return []QuerySet{}
} else {
// Finding final index of select((\s)+(all)|(distinct)(\s)+)|(\s)+ inside query_lower string
_, end_select := select_header.match_string(query_lower)
Comment on lines +278 to +282
Copy link
Member

@ttytm ttytm May 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As an aspect of readability, I would mention avoiding unnecessary nested code, with a reference to the book 100 Go Mistakes and How to Avoid Them.

When an if block returns, we should omit the else block [...]

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That should be common sense for any language.


// Finding initial and final index of (\s)+from(\s)+ inside query_lower string
init_from, end_from := from.find(query_lower)

// Get fields possibly separated by ',' like: select field_1, field_2 as f2, from table_name
fields := query.substr(end_select, init_from).split(',')

mut query_set := []QuerySet{}
mut tuple := map[string]string{}

if fields[0] == '*' {
table_name := query.substr(end_from, query.len).replace(';', '').split(' ')[0]

// Get all fields
all_columns_name := db.exec('pragma table_info(${table_name})')!

defer {
unsafe { all_columns_name.free() }
}

mut i := 0

for row in rows {
for i < row.vals.len {
// position 1 has a database column attribute
tuple[all_columns_name[i].vals[1]] = row.vals[i]
i++
}
i = 0

query_set << QuerySet{tuple}
tuple = map[string]string{}
}
} else {
mut i := 0

for row in rows {
for field in fields {
// verifying formats like:
// select column_1 as alias_column_1 from table_name -> alias creation with 'as'
// select column_1 alias_column_1 from table_name -> alias creationg without 'as'
// select column_1 from table_name -> with alias being column_1
all_field_alias := if field.contains('as') {
field.split('as')
} else {
field.split(' ')
}

alias := all_field_alias[all_field_alias.len - 1].replace(' ',
'')
tuple[alias] = row.vals[i]
i++
}

i = 0
query_set << QuerySet{tuple}
tuple = map[string]string{}
}
}

return query_set
}
} else {
return &SQLError{
msg: 'This is not a selection query or contains subqueries'
code: sqlite.sqlite_done
}
}
}

// 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]
Expand Down
70 changes: 68 additions & 2 deletions vlib/db/sqlite/sqlite_test.v
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ fn test_sqlite() {
assert users.len == 4
code := db.exec_none('vacuum')
assert code == 101
user := db.exec_one('select * from users where id = 3') or { panic(err) }
user := db.exec_one('select * from users where id = 3')!
println(user)
assert user.vals.len == 2

Expand All @@ -79,7 +79,73 @@ fn test_sqlite() {
db.exec("delete from users where name='Sam'")!
assert db.get_affected_rows_count() == 1

db.close() or { panic(err) }
db.get_queryset('SELECT * FROM users')!
assert db.get_affected_rows_count() == 1

db.get_queryset('Select * From users')!
assert db.get_affected_rows_count() == 1

db.get_queryset('select * from users')!
assert db.get_affected_rows_count() == 1

db.get_queryset('SELECT * FROM users')!
assert db.get_affected_rows_count() == 1

db.get_queryset('Select * From users')!
assert db.get_affected_rows_count() == 1

db.get_queryset('select * from users')!
// assert db.get_affected_rows_count() == 1

db.get_queryset('SELECT name, id FROM users WHERE id = 3')!
assert db.get_affected_rows_count() == 1

db.get_queryset('Select name as n From users WHERE id = 3')!
assert db.get_affected_rows_count() == 1

db.get_queryset('select name n from users WHERE id = 3')!
assert db.get_affected_rows_count() == 1

db.get_queryset('SELECT name, id FROM users WHERE id = 3')!
assert db.get_affected_rows_count() == 1

db.get_queryset('Select name as name From users WHERE id = 3')!
assert db.get_affected_rows_count() == 1

db.get_queryset('select name n from users WHERE id = 3')!
assert db.get_affected_rows_count() == 1

db.get_queryset('SELECT * FROM users WHERE id = 3')!
assert db.get_affected_rows_count() == 1

db.get_queryset('select id from users WHERE id = 3')!
assert db.get_affected_rows_count() == 1

db.get_queryset('select id as i, name as n from users WHERE id = 3')!
assert db.get_affected_rows_count() == 1

db.get_queryset('select id _id, name _n from users WHERE id = 3')!
assert db.get_affected_rows_count() == 1

db.get_queryset('select max(id), name from users WHERE id = 3')!
assert db.get_affected_rows_count() == 1

db.get_queryset('select max(id) as max, name from users WHERE id = 3')!
assert db.get_affected_rows_count() == 1

db.get_queryset('select max(id) from users WHERE id = 3')!
assert db.get_affected_rows_count() == 1

db.get_queryset('select max(id) max, name n from users WHERE id = 3')!
assert db.get_affected_rows_count() == 1

db.get_queryset('select * from users;')!
assert db.get_affected_rows_count() == 1

db.get_queryset('select * from users ;')!
assert db.get_affected_rows_count() == 1

db.close()!
assert !db.is_open
}

Expand Down