Show me an Injection Attack for this Stored Procedure - mysql

I notice that many people have said it's possible to create an injection attack, but my understanding is that is if someone is creating a query from a string, not parameters. In order to test the statement that Stored Procedures do not protect you against Injection Attacks, I am putting this example up in the hopes someone can show me a vulnerability if there is one.
Please note that I have built the code this way to easily insert a function that calls a procedure and embed it in a SELECT query. That means I cannot create a Prepared Statement. Ideally I'd like to keep my setup this way, as it is dynamic and quick, but if someone can create an injection attack that works, obviously that is not going to happen.
DELIMITER $$
#This procedure searches for an object by a unique name in the table.
#If it is not found, it inserts. Either way, the ID of the object
#is returned.
CREATE PROCEDURE `id_insert_or_find` (in _value char(200), out _id bigint(20))
BEGIN
SET #_value = _value;
SET #id = NULL;
SELECT id INTO _id FROM `table` WHERE name=_value;
IF _id IS NULL THEN
BEGIN
INSERT INTO `table` (`name`) VALUE (_value);
SELECT LAST_INSERT_ID() INTO _id;
END;
END IF;
END$$
CREATE FUNCTION `get_id` (_object_name char(200)) RETURNS INT DETERMINISTIC
BEGIN
SET #id = NULL;
call `id_insert_or_find`(_object_name,#id);
return #id;
END$$
The PHP Code
The PHP code I use here is:
(note, Boann has pointed out the folly of this code, below. I am not editing it for the sake of honoring the answer, but it will certainly not be a straight query in the code. It will be updated using ->prepare, etc. I still welcome any additional comments if new vulnerabilities are spotted.)
function add_relationship($table_name,$table_name_child) {
#This table updates a separate table which has
#parent/child relationships listed.
$db->query("INSERT INTO table_relationships (`table_id`,`tableChild_id`) VALUES (get_id('{$table_name}'),get_id('{$table_name_child}')");
}
The end result is
table `table`
id name
1 oak
2 mahogany
Now if I wanted to make oak the child of mahogany, I could use
add_relationship("mahogany","oak");
And if I wanted to make plastic the child of oak, I could use
add_relationship("oak","plastic");
Hopefully that helps give some framework and context.

It is not necessarily the stored procedure that is unsafe but the way you call it.
For example if you do the following:
mysqli_multi_query("CALL id_insert_or_find(" + $value + ", " + $id + ")");
then the attacker would set $value="'attack'" and id="1); DROP SCHEMA YOUR_DB; --"
then the result would be
mysqli_multi_query("CALL id_insert_or_find('attack', 1); DROP SCHEMA YOUR_DB; --)");
BOOM DEAD

Strictly speaking, that query should be written to escape the table names:
$db->query("INSERT INTO table_relationships (`table_id`,`tableChild_id`) " .
"VALUES (get_id(" . $db->quote($table_name) + ")," .
"get_id(" . $db->quote($table_name_child) . "))");
Otherwise, it would break out of the quotes if one of the parameters contained a single quote. If you only ever call that function using literal strings in code (e.g., add_relationship("mahogany", "oak");) then it is safe to not escape it. If you might ever call add_relationship using data from $_GET/$_POST/$_COOKIE or other database fields or files, etc, it's asking for trouble. I would certainly not let it pass a code review.
If a user could control the table name provided to that function then they could do, for example:
add_relationship("oak", "'+(SELECT CONCAT_WS(',', password_hash, password_salt) FROM users WHERE username='admin')+'");
Now you might say that there's no practical way to then extract that information if the resulting table name doesn't exist, but even then you could still extract information one binary bit at a time using a binary search and separate queries, just by breaking the query. Something like this (exact syntax not tested):
add_relationship("oak", "plastic'+(IF(ORD(SUBSTR(SELECT password_hash FROM users WHERE username='admin'),1,1)>=128, 'foo', ''))+'");
Really, it's easier to just escape the parameters and then you don't have to worry.

Related

Is there a way to change the default date input format

I have a source of data from where I extract some fields, among the fields there are some date fields and the source sends their values like this
#DD/MM/YYYY#
almost all the fields can be sent into the query with no modificaction, except this of course.
I have written a program the gets the data from an internet connection and sends it to the MySQL server and it's sending everything as it should, I am sure because I enabled general logging in the MySQL server and I can see all the queries are correct, except the ones with date fields.
I would like to avoid parsing the fields this way because it's a lot of work since it's all written in c, but if there is no way to do it, I understand and would accept that as an answer of course.
As an example suppose we had the following
INSERT INTO sometable VALUES ('#12/10/2015#', ... OTHER_VALUES ..., '#11/10/2015#');
in this case I send the whole thing as a query using mysql_query() from libmysqlclient.
In other cases I can split the parts of the message in something that is like an instruction and the parameters, something like this
iab A,B,C,#12/10/2015#,X,Y,#11/10/2015#
which could mean INSERT INTO table_a_something_b_whatever VALUES, and in this situation of course, I capture all the parameters and send a single query with a list of VALUES in it. Also in this situation, it's rather simple because I can handle the date like this
char date[] = "#11/10/2015#";
int day;
int month;
int year;
if (sscanf(date, "#%d/%d/%d#", &day, &month, &year) == 3)
{
/* it's fine, build a sane YYYY-MM-DD */
}
So the question is:
How can I tell the MySQL server in what format the date fields are?
Clarification to: Comment 1
Not necessarily INSERT, it's more complex than that. They are sometimes queries with all their parameters in it, sometimes they are just the parameters and I have to build the query. It's a huge mess but I can't do anything about it because it's a paid database and I must use it for the time being.
The real problem is when the query comes from the source and has to be sent as it is, because then there can be many occurrences. When I split the parameters one by one there is no real problem because parsing the above date format and generating the appropriate value of MySQL is quite simple.
You can use STR_TO_DATE() in MySQL:
SELECT STR_TO_DATE('#08/10/2015#','#%d/%m%Y#');
Use this as part of your INSERT process:
INSERT INTO yourtable (yourdatecolumn) VALUES (STR_TO_DATE('#08/10/2015#','#%d/%m%Y#'));
The only Thing I could imagine at the Moment would be to Change your Column-Type from DateTime to varchar and use a BEFORE INSERT Trigger to fix "wrong" Dates.
Something like this:
DELIMITER //
CREATE TRIGGER t1 BEFORE INSERT on myTable FOR EACH ROW
BEGIN
IF (NEW.myDate regexp '#[[:digit:]]+\/[[:digit:]]+\/[[:digit:]]+#') THEN
SET NEW.myDate = STR_TO_DATE(NEW.myDate,'#%d/%m/%Y#');
END IF;
END; //
DELIMITER ;
If you are just Need to run the Import in question once, use the Trigger to generate a "proper" dateTimeColumn out of the inserts - and drop the varchar-column afterwards:
('myDate' := varchar column to be dropped afterwards;`'myRealDate' := DateTime Column to Keep afterwards)
DELIMITER //
CREATE TRIGGER t1 BEFORE INSERT on myTable FOR EACH ROW
BEGIN
IF (NEW.myDate regexp '#[[:digit:]]+\/[[:digit:]]+\/[[:digit:]]+#') THEN
SET NEW.myRealDate = STR_TO_DATE(NEW.myDate,'#%d/%m/%Y#');
else
#assume a valid date representation
SET NEW.myRealDate = NEW.myDate;
END IF;
END; //
DELIMITER ;
Unfortunately you cannot use a Trigger to work on the datetime-column itself, because mysql will already mess up the NEW.myDate-Column.

sql injection operator explanation [duplicate]

Just looking at:
(Source: https://xkcd.com/327/)
What does this SQL do:
Robert'); DROP TABLE STUDENTS; --
I know both ' and -- are for comments, but doesn't the word DROP get commented as well since it is part of the same line?
It drops the students table.
The original code in the school's program probably looks something like
q = "INSERT INTO Students VALUES ('" + FNMName.Text + "', '" + LName.Text + "')";
This is the naive way to add text input into a query, and is very bad, as you will see.
After the values from the first name, middle name textbox FNMName.Text (which is Robert'); DROP TABLE STUDENTS; --) and the last name textbox LName.Text (let's call it Derper) are concatenated with the rest of the query, the result is now actually two queries separated by the statement terminator (semicolon). The second query has been injected into the first. When the code executes this query against the database, it will look like this
INSERT INTO Students VALUES ('Robert'); DROP TABLE Students; --', 'Derper')
which, in plain English, roughly translates to the two queries:
Add a new record to the Students table with a Name value of 'Robert'
and
Delete the Students table
Everything past the second query is marked as a comment: --', 'Derper')
The ' in the student's name is not a comment, it's the closing string delimiter. Since the student's name is a string, it's needed syntactically to complete the hypothetical query. Injection attacks only work when the SQL query they inject results in valid SQL.
Edited again as per dan04's astute comment
Let's say the name was used in a variable, $Name. You then run this query:
INSERT INTO Students VALUES ( '$Name' )
The code is mistakenly placing anything the user supplied as the variable. You wanted the SQL to be:
INSERT INTO Students VALUES ( 'Robert Tables` )
But a clever user can supply whatever they want:
INSERT INTO Students VALUES ( 'Robert'); DROP TABLE Students; --' )
What you get is:
INSERT INTO Students VALUES ( 'Robert' ); DROP TABLE STUDENTS; --' )
The -- only comments the remainder of the line.
As everyone else has pointed out already, the '); closes the original statement and then a second statement follows. Most frameworks, including languages like PHP, have default security settings by now that don't allow multiple statements in one SQL string. In PHP, for example, you can only run multiple statements in one SQL string by using the mysqli_multi_query function.
You can, however, manipulate an existing SQL statement via SQL injection without having to add a second statement. Let's say you have a login system which checks a username and a password with this simple select:
$query="SELECT * FROM users WHERE username='" . $_REQUEST['user'] . "' and (password='".$_REQUEST['pass']."')";
$result=mysql_query($query);
If you provide peter as the username and secret as the password, the resulting SQL string would look like this:
SELECT * FROM users WHERE username='peter' and (password='secret')
Everything's fine. Now imagine you provide this string as the password:
' OR '1'='1
Then the resulting SQL string would be this:
SELECT * FROM users WHERE username='peter' and (password='' OR '1'='1')
That would enable you to log in to any account without knowing the password. So you don't need to be able to use two statements in order to use SQL injection, although you can do more destructive things if you are able to supply multiple statements.
No, ' isn't a comment in SQL, but a delimiter.
Mom supposed the database programmer made a request looking like:
INSERT INTO 'students' ('first_name', 'last_name') VALUES ('$firstName', '$lastName');
(for example) to add the new student, where the $xxx variable contents was taken directly out of an HTML form, without checking format nor escaping special characters.
So if $firstName contains Robert'); DROP TABLE students; -- the database program will execute the following request directly on the DB:
INSERT INTO 'students' ('first_name', 'last_name') VALUES ('Robert'); DROP TABLE students; --', 'XKCD');
ie. it will terminate early the insert statement, execute whatever malicious code the cracker wants, then comment out whatever remainder of code there might be.
Mmm, I am too slow, I see already 8 answers before mine in the orange band... :-) A popular topic, it seems.
TL;DR
-- The application accepts input, in this case 'Nancy', without attempting to
-- sanitize the input, such as by escaping special characters
school=> INSERT INTO students VALUES ('Nancy');
INSERT 0 1
-- SQL injection occurs when input into a database command is manipulated to
-- cause the database server to execute arbitrary SQL
school=> INSERT INTO students VALUES ('Robert'); DROP TABLE students; --');
INSERT 0 1
DROP TABLE
-- The student records are now gone - it could have been even worse!
school=> SELECT * FROM students;
ERROR: relation "students" does not exist
LINE 1: SELECT * FROM students;
^
This drops (deletes) the student table.
(All code examples in this answer were run on a PostgreSQL 9.1.2 database server.)
To make it clear what's happening, let's try this with a simple table containing only the name field and add a single row:
school=> CREATE TABLE students (name TEXT PRIMARY KEY);
NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "students_pkey" for table "students"
CREATE TABLE
school=> INSERT INTO students VALUES ('John');
INSERT 0 1
Let's assume the application uses the following SQL to insert data into the table:
INSERT INTO students VALUES ('foobar');
Replace foobar with the actual name of the student. A normal insert operation would look like this:
-- Input: Nancy
school=> INSERT INTO students VALUES ('Nancy');
INSERT 0 1
When we query the table, we get this:
school=> SELECT * FROM students;
name
-------
John
Nancy
(2 rows)
What happens when we insert Little Bobby Tables's name into the table?
-- Input: Robert'); DROP TABLE students; --
school=> INSERT INTO students VALUES ('Robert'); DROP TABLE students; --');
INSERT 0 1
DROP TABLE
The SQL injection here is the result of the name of the student terminating the statement and including a separate DROP TABLE command; the two dashes at the end of the input are intended to comment out any leftover code that would otherwise cause an error. The last line of the output confirms that the database server has dropped the table.
It's important to notice that during the INSERT operation the application isn't checking the input for any special characters, and is therefore allowing arbitrary input to be entered into the SQL command. This means that a malicious user can insert, into a field normally intended for user input, special symbols such as quotes along with arbitrary SQL code to cause the database system to execute it, hence SQL injection.
The result?
school=> SELECT * FROM students;
ERROR: relation "students" does not exist
LINE 1: SELECT * FROM students;
^
SQL injection is the database equivalent of a remote arbitrary code execution vulnerability in an operating system or application. The potential impact of a successful SQL injection attack cannot be underestimated--depending on the database system and application configuration, it can be used by an attacker to cause data loss (as in this case), gain unauthorized access to data, or even execute arbitrary code on the host machine itself.
As noted by the XKCD comic, one way of protecting against SQL injection attacks is to sanitize database inputs, such as by escaping special characters, so that they cannot modify the underlying SQL command and therefore cannot cause execution of arbitrary SQL code. This can be done at the application level, and some implementations of parameterized queries operate by sanitizing input.
However, sanitizing inputs at the application level may not stop more advanced SQL injection techniques. For example, there are ways to circumvent the mysql_real_escape_string PHP function. For added protection, many database systems support prepared statements. If properly implemented in the backend, prepared statements can make SQL injection impossible by treating data inputs as semantically separate from the rest of the command.
Say you naively wrote a student creation method like this:
void createStudent(String name) {
database.execute("INSERT INTO students (name) VALUES ('" + name + "')");
}
And someone enters the name Robert'); DROP TABLE STUDENTS; --
What gets run on the database is this query:
INSERT INTO students (name) VALUES ('Robert'); DROP TABLE STUDENTS --')
The semicolon ends the insert command and starts another; the -- comments out the rest of the line. The DROP TABLE command is executed...
This is why bind parameters are a good thing.
A single quote is the start and end of a string. A semicolon is the end of a statement. So if they were doing a select like this:
Select *
From Students
Where (Name = '<NameGetsInsertedHere>')
The SQL would become:
Select *
From Students
Where (Name = 'Robert'); DROP TABLE STUDENTS; --')
-- ^-------------------------------^
On some systems, the select would get ran first followed by the drop statement! The message is: DONT EMBED VALUES INTO YOUR SQL. Instead use parameters!
The '); ends the query, it doesn't start a comment. Then it drops the students table and comments the rest of the query that was supposed to be executed.
In this case, ' is not a comment character. It's used to delimit string literals. The comic artist is banking on the idea that the school in question has dynamic sql somewhere that looks something like this:
$sql = "INSERT INTO `Students` (FirstName, LastName) VALUES ('" . $fname . "', '" . $lname . "')";
So now the ' character ends the string literal before the programmer was expecting it. Combined with the ; character to end the statement, an attacker can now add (inject) whatever sql they want. The -- comment at the end is to make sure any remaining sql in the original statement does not prevent the query from compiling on the server.
FWIW, I also think the comic in question has an important detail wrong: if you sanitize your database inputs, as the comic suggests, you're still doing it wrong. Instead, you should think in terms of quarantining your database inputs, and the correct way to do this is via parameterized queries/prepared statements.
The writer of the database probably did a
sql = "SELECT * FROM STUDENTS WHERE (STUDENT_NAME = '" + student_name + "') AND other stuff";
execute(sql);
If student_name is the one given, that does the selection with the name "Robert" and then drops the table. The "-- " part changes the rest of the given query into a comment.
The ' character in SQL is used for string constants. In this case it is used for ending the string constant and not for comment.
This is how it works:
Lets suppose the administrator is looking for records of student
Robert'); DROP TABLE STUDENTS; --
Since the admin account has high privileges deleting the table from this account is possible.
The code to retrieve user name from request is
Now the query would be something like this (to search the student table)
String query="Select * from student where username='"+student_name+"'";
statement.executeQuery(query); //Rest of the code follows
The resultant query becomes
Select * from student where username='Robert'); DROP TABLE STUDENTS; --
Since the user input is not sanitized, The above query has is manipulated into 2 parts
Select * from student where username='Robert');
DROP TABLE STUDENTS; --
The double dash (--) will just comment out remaining part of the query.
This is dangerous as it can nullify password authentication, if present
The first one will do the normal search.
The second one will drop the table student if the account has sufficient privileges (Generally the school admin account will run such query and will have the privileges talked about above).
You don't need to input form data to make SQL injection.
No one pointed this out before so through I might alert some of you.
Mostly we will try to patch forms input. But this is not the only place where you can get attacked with SQL injection. You can do very simple attack with URL which send data through GET request;
Consider the fallowing example:
show something
Your url would look
http://yoursite.com/show?id=1
Now someone could try something like this
http://yoursite.com/show?id=1;TRUNCATE table_name
Try to replace table_name with the real table name. If he get your table name right they would empty your table! (It is very easy to brut force this URL with simple script)
Your query would look something like this...
"SELECT * FROM page WHERE id = 4;TRUNCATE page"
Example of PHP vulnerable code using PDO:
<?php
...
$id = $_GET['id'];
$pdo = new PDO($database_dsn, $database_user, $database_pass);
$query = "SELECT * FROM page WHERE id = {$id}";
$stmt = $pdo->query($query);
$data = $stmt->fetch();
/************* You have lost your data!!! :( *************/
...
Solution - use PDO prepare() & bindParam() methods:
<?php
...
$id = $_GET['id'];
$query = 'SELECT * FROM page WHERE id = :idVal';
$stmt = $pdo->prepare($query);
$stmt->bindParam('idVal', $id, PDO::PARAM_INT);
$stmt->execute();
$data = $stmt->fetch();
/************* Your data is safe! :) *************/
...

Sanitisation of input for a stored MySQL database procedure

I am using a database someone else produced (and I am not really authorised to change it). However, as I was looking into the stored procedures within the database I noticed the following procedure:
DELIMITER $$
CREATE PROCEDURE `logIn`(userName varChar(50), userPass varChar(50))
BEGIN
declare userID int;
SELECT
u.userID INTO userID
FROM
users u
WHERE
u.userName=userName
AND u.userPassword=MD5(userPass);
IF (IFNULL(uID,-1) > 0) THEN
select 1 as outMsg;
ELSE
select 0 as outMsg;
END IF;
END$$
with the corresponding table users having three columns: userID INT, userName VARCHAR(50) and userPassword VARCHAR(50).
As I am not very good at this, could someone let me know whether the input for such a function needs to be sanitised as to not allow any SQL injections and if not - why? A general rule of thumb would be very much appreciated.
P.S. This function will be called from a JS script on a form submit.
There are a few rules of thumb here that depend on the underlying datatype and how it's inserted into the database.
First, Parameterized queries are always best for SQL Injection protection.. but.. if you can't change that..
String type:
Remove any single quotes
OR
Replace any single quotes with the single quote twice.
Replace any of the following characters with their encoded alternative;
>
<
"
;
(chr 34)
)
(
For example.. ) is replaced with & #x29;
-(the space in the above example is so you'll see the code, remove it to get ")")
For a datatype other then string, check that the datatype is sane and remove any character that shouldn't be in the datatype. If it's an integer, make sure the string that you're passing in is an integer. This can commonly be done by casting to the type in code. The cast will either work.. or cause an error. It's also good to check that the datatype min and maxes have not been exceeded. For example.. If I was checking for an integer, I might use code similar to this:
var myInt = parseInt(param);
Then I might check it's bounds to be sure it's less then the maximum integer value and greater then the minimum integer value.
That should be good enough to prevent a SQL Injection attack...
And.. since you have not posted the code that actually interfaces with the database... As an added precaution.. you may also want to remove --,`,%,",", "".
You only want 'sane' values getting to the database call.. so an integer like, $309 wouldn't make sense, you'd want to remove the $.. . probably by using a regex replace for any non numeric characters a comma and a period.
[^[0-9,.]]
Be extra cautious.
Yes, the input must be sanitized before trying to run the procedure.
You might want to share the actual calling point for the procedure to get more help here, since there is no way that the procedure is called directly from JS on form submit. You probably have a Servlet, PHP page or some HTTP friendly intermediary to make the database call somehow.

Creating a stored procedure in SQL Server 2008 that will do a "facebook search"

I'm trying to implement a facebook search in my system (auto suggest while typing).
I've managed to code all the ajax stuff, but I'm not sure how to query the database.
I've created a table called People which contains the fields: ID, FirstName, LastName, MiddleName, Email.
I've also created a FTS-index on all those fields.
I want to create a stored procedure that will get as a parameter the text inserted in the query box and returns the suggestions.
For example, When I will write in the textbox the query "Joh Do"
It will translate to the query:
select * from People where contains(*, '"Joh*"') and contains(*, '"Do*"')
Is there a way to do that in stored procedure?
P.S
I've tried to use the syntax
select * from People where contains(*,'"Joh*" and "Do*"')
but it didn't returned the expected results, probably because it needs to search the words on different fields. Is there a way to fix that?
Thanks.
Try
select *
from People
where (FirstName Like '%'+ #FirstName + '%') and
(MiddleName Like '%'+ #MiddleName + '%') and
(LastName Like '%'+ #LastName + '%')
Also you may want to restrict the results to only return a maximum of say 10 by using:
select top 10
EDIT 1:
OK I now understand the problem better. I would use dynamic sql thus:
First create a split function e.g. Example Split function using XML trick
Then use dynamic sql:
declare #tstr varchar (500)
set #tstr = ''
select #tstr =#tstr + ' Contains(*, ''"'+ val + '*")' + ' and '
from dbo.split(#SearchStr, ' ')
set #tstr = left(#tstr,len(#tstr)-4)
select #tstr
Declare #dsql as varchar(500)
set #dsql = 'select * from People where '+ #tstr
exec (#dsql)
Also please note as per Remus, be aware of SQL Injections, the use of sp_executesql (instead of EXEC) would be better.
The problem is the open list nature of the argument. I can be Joh, it can be Joh Do, it can be Joh Do Na and so on and so forth. You have two main alternatives:
parse the input in the web app (in ASP I assume) and then call an appropriate procedure for the number of entries (ie. exec usp_findSuggestions1 'Joh', exec usp_findSuggestions2 'Joh', 'Do', exec usp_findSuggestions1 'Joh', 'Do', 'Na'). The first procedure uses 1 contains, the second has 2 contains .. and contains ... and the last has 3. This may look totally ugly from a DRY, code design and specially code maintenance pov, but is actually the best solution as far as T-SQL is concerned, due primarily to the plan stability of these queries.
pass the input straight into a single stored procedure, where you can split it into components and build a dynamic T-SQL query with as many contains as necessary.
Both solutions are imperfect. Ultimately, you have two problems, and both have been investigated before to quite some depth:
the problem of passing a list to a T-SQL procedure. See Arrays and Lists in SQL Server 2005 and Beyond
the problem of an undetermined number of conditions in the WHERE clause, see The Curse and Blessings of Dynamic SQL
The AJAX Toolkit has the "AutoComplete" control that provides this functionality out of the box. It is very simple to use.
Look at a sample here

Storing SQL field names and general SQL usage with Delphi

I'm starting to write my first Delphi application that connects to an SQL database (MySQL) using the ADO database components. I wondered whether there was any best way of storing the names of the fields in the database for easy reference when creating SQL queries later on.
First of all I made them a simple constant e.g. c_UserTable_Username, c_UserTable_Password, but then decided that was not a particularly good way of doing things so I am now storing them in a constant record e.g.:
type
TUserTable = record
TableName : String;
Username : String;
Password : String;
end;
const
UserTable : TUserTable =
(
TableName : 'users';
Username : 'Username';
Password : 'Password';
);
this allows me to create a statement like:
query.SQL.Add('SELECT ' + UserTable.Username + ' FROM ' + UserTable.TableName);
and not have to worry about hard coding the field names etc.
I've now run into the problem however where if I want to cycle through the table fields (for example if there are 20 or so fields), I can't. I have to manually type the record reference for every field.
I guess what I'd like to know is whether there is a way to iterate though all field names at once, or singularly; or am I going about this the wrong way? Perhaps I shouldn't be storing them like this at all?
Also, I've made a “Database” class which basically holds methods for many different SQL statements, for example GetAllUsers, GetAllProducts, etc. Does that sound correct? I've taken a look at a lot of Delphi/SQL tutorials, but they don't seem to go much past showing you how to run queries.
I suppose I'm just a little lost and any help is very welcome. Thanks :)
You could also store your queries as RESOURCESTRING which would allow editing of them after the fact using a resource editor (if necessary).
RESOURCESTRING
rsSelectFromUsers = 'SELECT USERNAME FROM USERS ';
Your approach of a database class works very well. I have done just that in several of my projects, returning an interface to an object which contains the dataset...the advantage of this is when the returned interface variable goes out of scope, the dataset would be closed and cleared.
Well, you are hard coding field names; you just hardcode them in the const instead of in the query itself. I'm not sure that actually improves anything. As far as iterating through the fields goes, try this:
var
Field: TField;
begin
for Field in query.Fields do begin
// do stuff with Field
end;
end;
Rather than making a "Database" class, I would probably use a TDataModule. This does almost the same thing as your class, except that it allows you to interactively design queries at design time. You can put any methods you need on the DataModule.
This also makes it really easy to instantiate persistent TFields (see help on that topic), which you may find the solution more to your liking than using consts to store field names.
If you're really going to use a database class as illustrated, consider the ability of records to contain functions in D2007 and later.
For instance, your example would become:
type
TUserTable = record
TableName : String;
Username : String;
Password : String;
function sqlGetUserName(where:string=''):string;
end;
const
UserTable : TUserTable =
(
TableName : 'users';
Username : 'Username';
Password : 'Password';
);
function TUserTable.sqlGetUserName(where:string=''): string;
begin
if where='' then result := Format('SELECT %s from %s', [userName, tableName])
else result := Format('SELECT %s from %s where %s', [userName, tableName, where]);
end;
which allows:
query.SQL.add(userTable.sqlGetUserName);
or
query.SQL.add(userTable.sqlGetUserName(Format('%s=%s', [userTable.userName,'BOB']));
I don't really recommend using SQL directly as you've illustrated. In my opinion, you should never have direct SQL calls to the tables. That's introducing a lot of coupling between the UI and the database (which shouldn't exist) and prevents you from placing a high level of security on direct table modification.
I would wrap everything into stored procs and have a DB interface class that encapsulates all of the database code into a data module. You can still use the direct links into data-aware components from a data module, you just have to preface the links with the DM name.
For instance, if you built a class like:
type
TDBInterface = class
private
function q(s:string):string; //just returns a SQL quoted string
public
procedure addUser(userName:string; password:string);
procedure getUser(userName:string);
procedure delUser(userName:string);
function testUser:boolean;
procedure testAllDataSets;
end;
function TDBInterface.q(s:string):string;
begin
result:=''''+s+'''';
end;
procedure TDBInterface.addUser(userName:string; password:string);
begin
cmd.CommandText:=Format( 'if (select count(userName) from users where userName=%s)=0 '+
'insert into users (userName, password) values (%s,%s) '+
'else '+
'update users set userName=%s, password=%s where userName=%s',
[q(userName), q(userName), q(password), q(userName), q(password), q(userName)]);
cmd.Execute;
end;
procedure TDBInterface.getUser(userName:string);
begin
qry.SQL.Add(Format('select * from users where userName=%s', [q(userName)]));
qry.Active:=true;
end;
procedure TDBInterface.delUser(userName:string);
begin
cmd.CommandText:=Format('delete from users where userName=%s',[userName]);
cmd.Execute;
end;
procedure TDBInterface.testAllDataSets;
begin
assert(testUser);
end;
function TDBInterface.testUser: boolean;
begin
result:=false;
addUser('99TEST99','just a test');
getUser('99TEST99');
if qry.IsEmpty then exit;
if qry.FieldByName('userName').value<>'99TEST99' then
exit;
delUser('99TEST99');
if qry.IsEmpty then
result:=true;
end;
You now have the ability to do some form of unit testing on your data interface, you've removed the SQL from the UI and things are looking up. You still have a lot of ugly SQL in your interface code though so move that over to stored procs and you get:
type
TDBInterface = class
public
procedure addUser(userName:string; password:string);
procedure getUser(userName:string);
procedure delUser(userName:string);
function testUser:boolean;
procedure testAllDataSets;
end;
procedure TDBInterface.addUser(userName:string; password:string);
begin
cmd.CommandText:='usp_addUser;1';
cmd.Parameters.Refresh;
cmd.Parameters.ParamByName('#userName').Value:=userName;
cmd.Parameters.ParamByName('#password').Value:=password;
cmd.Execute;
cmd.Execute;
end;
procedure TDBInterface.getUser(userName:string);
begin
sproc.Parameters.ParamByName('#userName').Value:=userName;
sproc.Active:=true;
end;
procedure TDBInterface.delUser(userName:string);
begin
cmd.CommandText:='usp_delUser;1';
cmd.Parameters.Refresh;
cmd.Parameters.ParamByName('#userName').Value:=userName;
cmd.Execute;
end;
You could now move some of these functions into an ADO Thread and the UI would have no idea that adding or deleting users occur in a separate process. Mind, these are very simple operations so if you want to do handy things like notifying the parent when process are done (to refresh a user list for instance after add/delete/update occurs), you'd need to code that into the threading model.
BTW, the stored proc for the add code looks like:
create procedure [dbo].[usp_addUser](#userName varchar(20), #password varchar(20)) as
if (select count(userName) from users where userName=#userName)=0
insert into users (userName, password) values (#userName,#password)
else
update users set userName=#userName, password=#password where userName=#userName
Also, a little disclaimer: this post is pretty long and, while I tried to check most of it, I may have missed something, somewhere.
Maybe a bit off topic but you could use Data Abstract from RemObjects.
Take a loot at Analysing DataSets (in Delphi and Kylix)
The code is a good example of manipulating table metadata. You can get field names and then write a code-generator that can create a base unit/ or the interface part of it.