I need some help writing basic Ruby code to register a user in a SQLite database. I'm very new to Ruby, I checked lots of good examples online but my code still doesn't work.
This is my 1st test project using Ruby, so appreciate any help and apologise for making any bad mistakes.
require 'sqlite3'
def register_user(l)
user = l[1]
pass = l[2]
db = SQLite3::Database.new "database.db"
db.execute("INSERT INTO users (user, pass)
VALUES (#{user}, #{pass})")
end
def cmd_register(l)
if register_user(#{#nick}, l[1])
sv_send 'NOTICE', 'REGISTER', ':*** User created'
else
sv_send 'NOTICE', 'REGISTER', ':*** User not created'
end
end
There are a few problems with your code. First, here:
db.execute("INSERT INTO users (user, pass)
VALUES (#{user}, #{pass})")
You're trying to generate a query that looks like this (supposing the variable user contains "Jordan" and pass contains "xyz"):
INSERT INTO users (user, pass) VALUES('Jordan', 'xyz')
...but your code generates a query that looks like this:
INSERT INTO users (user, pass) VALUES(Jordan, xyz)
Do you see the difference? Values in SQL queries need to be surrounded by quotation marks. Your query will fail because SQLite doesn't know what Jordan is; it only knows what 'Jordan' is.
You could just add quotation marks to your query, but then you would have another problem: SQL injection attacks. Because you're just blindly putting the values of user and pass into your query, an attacker could manipulate those values to perform a different query than you intended. Never use string interpolation (#{var}) or concatenation (+ or <<) when creating an SQL query. (For a brief description of how SQL injection attacks work, read the "How to get hacked" section on this page: http://ruby.bastardsbook.com/chapters/sql/.)
The correct way to use variables in a query is with prepared statements and parameter binding. It looks like this:
statement = db.prepare("INSERT INTO users (user, pass) VALUES (?, ?)")
statement.bind_params(user, pass)
result = statement.execute
What this does is automatically escapes the values of user and pass to make sure they don't do anything you don't expect, wraps them in quotation marks, and substitutes them for the question marks in the query. Another way to do the same thing is this:
result = db.execute("INSERT INTO users (user, pass) VALUES (?, ?)", user, pass)
The other obvious problem with your code is this:
if register_user(#{#nick}, l[1])
This is a syntax error. You can only use the #{var} syntax in a string, like "hello #{var}". In this case you just want to do this:
if register_user(#nick, l[1])
require "sqlite3"
my_db = SQLite3::Database.new "my_db1.db"
my_db.execute <<END_OF_CREATE #The <<END_OF_CREATE .... END_OF_CREATE thing is called HEREDOC syntax, which is one way to create a String that spans multiple lines
CREATE TABLE IF NOT EXISTS users( #A useful SQL command to be aware of.
name varchar(30),
password varchar(30)
);
END_OF_CREATE
def register_user(target_db, user_info)
user_name, user_pass = user_info #A neat trick for "unpacking" an Array
target_db.execute(
"INSERT INTO users (name, password)
VALUES (?, ?)", user_name, user_pass #For security reasons, inserts into a db should use this "question mark" format.
)
end
register_user(my_db, ['John', 'abc123'])
register_user(my_db, ['Jane', 'xyz456'])
my_db.execute("SELECT * FROM users") do |row|
p row #Use the 'p' method instead of puts to print an Array or Hash.
end
--output:--
["John", "abc123"]
["Jane", "xyz456"]
Also, don't ever name a variable l. You absolutely, no exceptions, have to use descriptive variable names. See the code above for an example.
Even though the code above unpacks the user_info array into separate variables, that is actually not required because execute() will take an
Array as an argument:
target_db.execute(
"INSERT INTO users (name, password)
VALUES (?, ?)", user_info
)
In other words, all the values for the question marks can be gathered into an Array and provided as the last argument for execute().
One problem you can run into when writing and testing database programs is when you change one of the column names in your table. The code above will cause an error: the table will not be re-created because the table already exists, but your new code will use the new column name, which won't exist in the table.
So, you might consider using this combination of sql statements:
my_db.execute <<END_OF_DROP
DROP TABLE IF EXISTS users
END_OF_DROP
my_db.execute <<END_OF_CREATE
CREATE TABLE users(
name varchar(30),
password varchar(30)
);
END_OF_CREATE
With those sql statements, if you change one of the column names (or add a column), then your new code won't throw an error because the table is destroyed and recreated with the new column names every time you run your program.
Related
I am trying to create a user-auth system and have run into a problem. In this code, I am trying to check if a certain value matches another value in the same row. It prints but does not work as a string when I set it equal to another string.
db.execute("CREATE TABLE IF NOT EXISTS auth(id int, name text, password text)")
db.execute("INSERT INTO auth(id, name, password) VALUES(?, ?, ?)", [1, 'Bugs', 'Carrots'])
t = db.execute("SELECT password FROM auth WHERE name = 'Bugs'")
if t == 'Carrots'
puts "yes"
end
#i am trying to print yes to the console
I think you're getting back an array representing the list of matches, although in this case there's only one row returned. Furthermore, the row itself is an array of values, although in this case the row will be an array of only one value. So you should be comparing t[0][0] rather than just t
You could use the function db.get_first_value instead to simplify this.
(This assumes that name is unique; you probably want to use a UNIQUE constraint in the database schema for both id and name.)
(And, yes, you should take #tadman's warning to heart.)
So here's a minimally altered version of the code you posted above (without the suggested improvements) that gives the desired result:
#!/usr/bin/env ruby
require 'sqlite3'
db = SQLite3::Database.new(':memory:')
db.execute("CREATE TABLE IF NOT EXISTS auth(id int, name text, password text)")
db.execute("INSERT INTO auth(id, name, password) VALUES(?, ?, ?)",
[1, 'Bugs', 'Carrots'])
t = db.execute("SELECT password FROM auth WHERE name = 'Bugs'")
if t[0][0] == 'Carrots'
puts "yes"
end
I would like to join two query result.
First one is
Gig.where(id:gigsInRadioIDS)
When I convert result to json I get this result
[{"id":1,"address":"test1"},{"id":2,"address":"test2"}
Second is.
Slot.where(status:"available").where(Sequel.lit('("from" > ?) AND ("to" < ?)', fromdate, todate))
when I convert result to json I get this result
[{"id":15,"status":"available","gig_id":1}]
What I want is to inner join to result
so I hope to get this result(I abbreviated some non related snippet)
[{"id":15,"status":"available","gig_id":1,"id":1,"address":"test1"}]
What I have tried is
Gig.where(id:gigsInRadioIDS).as(:tb_gig).join(Slot.where(status:"available").where(Sequel.lit('("from" > ?) AND ("to" < ?)', fromdate, todate)), gig_id: :id)
And I got this error
Sequel::DatabaseError - PG::AmbiguousColumn: ERROR: column reference "id" is ambiguous
LINE 1: ...) AS "t1" ON ("t1"."gig_id" = "gigs"."id") WHERE ("id" IN (2...
Thanks for reading this complex code.
Any news will be big help. Thanks.
There are a bunch of ways to approach it. It looks like you are querying Slots with some Gigs data mixed in, so you may want to start with your Slot model. The regular join syntax is a little easier to understand and makes it easier to select columns from both tables (i.e. instead of WHERE fk IN (..)). As a bonus, you can mix your gig_id filter into the JOIN clause. Finally, you don't need to use Sequel.lit to do greater than / less than in your WHERE clause, and you should be explicit about the columns you want to select.
Here's how I would write it:
Slot.join(:gigs, [[:id, :gig_id], [:id, gigsInRadioIDS]])
.where(status: 'available') { (from() > fromdate) & (to() < todate) }
.select(Sequel[:slots][:id].as(:slot_id), :status, :gig_id, :address)
Getting .all of the records and outputting JSON should yield something like:
[{"slot_id":15,"status":"available","gig_id":1,"address":"test1"}]
Note the Sequel[:table_name][:column_name] syntax. This is the new style of fully-qualifying identifiers in Sequel 4.49+. (In past versions of Sequel, it would have been written like :table_name__column_name, but that syntax is now deprecated.)
Your where(id:gigsInRadioIDS) translates into WHERE ("id" IN (...)), and since both gigs and slots have an "id" column, the database doesn't know which id you want.
You need to explicitly specify the table with where{{gigs[:id] => gigsInRadioIDS}}
Querying with Sequel
I am using Sinatra and Sequel with PostgreSQL.
After authentication, I want to welcome the user by printing their name but I cannot get only the value of the user's name from the database, it comes out as a hash.
The query is:
current_user = DB[:users].select(:username).where('password = ?', password).first
and the resulting piece of data is:
Welcome, {:username=>"Rich"}
which looks rather weird, I would prefer it to read "Welcome, Rich".
What am I doing wrong here? I tried the same query without 'first" at the end and that does not work either.
You can either pull the (single) column you selected out of the Hash you are given:
current_user = DB[:users].select(:username).where('password=?', password).first[:username]
Or you can map your results to an array of usernames and pull the first:
# Using a hash in the filter method is simpler than SQL placeholders.
current_user = DB[:users].filter(password:password).select_map(:username).first
But the best way is to get only the user you care about, and then get the name:
# Using [] on a dataset returns the first row matching the criteria
current_user = DB[:users][password:password][:username]
Try Sequel::Dataset#get. Also, as Phrogz points out, Sequel::Dataset#where can take a hash (it will securely escape values to prevent injection attacks).
current_username = DB[:users].where(password: password).get(:username)
There's also Sequel::Dataset#where_single_value, which is optimized for this exact situation:
current_username = DB[:users].select(:username).where_single_value(password: password)
So I have 2 tables actor and actor2role. The latter is a lookup (junction) table to relate actor, role and dvd. I need to create a query with aliases, so I have this method:
def self.remove_duplicate_by_id(id)
offendingActor = self.find(id).actor # get the actor's name
ids = self.find_by_sql("SELECT MIN(id) AS minId, MAX(id) AS maxId, actor FROM `dvd_actor` WHERE actor = '#{offendingActor}'")
rolesForOffender = ids.actor2role # throws error
end
The problem is that ids is not an ActiveRecord object so I can't use the actor2role method (which is a relationship I've established between the 2 tables in Rails and works when you do something like Actor.first.actor2role.
so the questions is: Am I doomed to do this manually and then issue another sql query to recreate what the actor2role method would accomplish or is there some way to do this with Rails objects?
I'd really like to do it all natively if possible because I also have to issue these queries:
UPDATE dvd_actor2role SET actorid = $d->minId WHERE actorId = $d->maxId");
DELETE FROM dvd_actor2role WHERE actorId = $d->maxId LIMIT 1");
Is this even possible?
In the end I went with this which seems to do the trick. If anyone can spot any code that could be optimized, or something inherently wrong (and feels like chiming in) please feel free to comment.
actorObject = self.find_by_id(id) # get the object because we need it below for other queries
offendingActor = actorObject.actor
ids = self.select("MIN(id) AS minId, MAX(id) AS maxId, id, actor").find_by_actor(offendingActor)
rolesForOffender = actorObject.actor2role
rolesForOffender.each do |r|
# first find out if the relationship already exists or we get a SQL error for the foreign key relationship.
exists = Actor2role.where("actorId = ? AND roleId = ?", ids.minId, r.roleId)
if exists.nil?
Actor2role.update_all("actorId = #{ids.minId}, actorId = #{ids.maxId}")
end
end
self.destroy(ids.maxId) # delete this guy in actor table
end
I'm using the Ruby Sqlite3 library to insert some records into a database. I'm having issues getting the statements below to work (I've tried all variants that are posted); I get a SQLite3::RangeException - bind or column index out of range: exception.
The query works if I just hardcode values into it... so what am I doing wrong?
statement = db.prepare("insert into IntegrationLogin (Username, Password, ProjectID) values (\"?1\", \"?2\", 1)")
statement.execute [params['username'], params['password']]
statement = db.prepare("insert into IntegrationLogin (Username, Password, ProjectID) values (\"?1\", \"?2\", 1)")
statement.execute params['username'], params['password']
statement = db.prepare("insert into IntegrationLogin (Username, Password, ProjectID) values (\"?\", \"?\", 1)")
statement.execute params['username'], params['password']
I think your problem lies with trying to encapsulate the parameter indicators (?) in quotes. This is unnecessary as the driver will wrap the string parameter in quotes and perform any additional escaping before executing the query. When I removed the quotes you example queries executed fine.
It looks like your superfluous quotations are escaping the parameter indicators somehow and the driver isn't seeing them.
Use this:
statement = db.prepare("insert into IntegrationLogin (Username, Password, ProjectID) values (?, ?, 1)")
You were making this harder than it needed to be. Don't worry, this kind of parameter binding completely prevents SQL injection, even without the extra quotes.