Perl: How to retrieve field names when doing $dbh->selectall_..? - perl

$sth = $dbh->prepare($sql);
$sth->execute();
$sth->{NAME};
But how do you do that when:
$hr = $dbh->selectall_hashref($sql,'pk_id');
There's no $sth, so how do you get the $sth->{NAME}? $dbh->{NAME} doesn't exist.

When you're looking at a row, you can always use keys %$row to find out what columns it contains. They'll be exactly the same thing as NAME (unless you change FetchHashKeyName to NAME_lc or NAME_uc).

You can always prepare and execute the handle yourself, get the column names from it, and then pass the handle instead of the sql to selectall_hashref (e.g. if you want the column names but the statement may return no rows). Though you may as well call fetchall_hashref on the statement handle.

Related

Combining rows from multiple sources in Virtual list Filemaker

I am trying to make an Excel-like 'pivot table' in Filemaker using a Virtual List as the source of the data. The issue is I want to be able to have 'categories' down the first column that aren't fixed. The field names won't work.
My current thought is to have a table with a field of the layout name and the categories, that I would combine with the rest of the data via a ExecuteSQL (or other function).
I can get it to work with two ExecuteSQL statements, one for the categories and one for the 'bulk' of the data, using text for the WHERE in the categories eSQL, then I combine them into one and I'm set.
My issue is that I'd like to be able to get the categories using a get(LayoutName) function, making the script more flexible. Whenever I use the get(LayoutName) in the WHERE line of the SQL I get a ? for the result. I have also tried putting the layout name in a field using a get(LayoutName), then using that field as in the WHERE statement, but that also returns an error.
I admit I am a bit of a newbie at this, so the problem is likely between the keyboard and the chair with a simple syntax error. I have tried a bunch of different ways with quotes, no quotes, single quotes, etc.
Here is what I am using for pulling the categories...
Substitute ( ExecuteSQL ( "SELECT Category_List
FROM Categories_VL
WHERE Layout_Name = Get(LayoutName)" ; "" ; "") ; ", " ; $$delim )
All of the field names are correct, and if I change the LayoutName to a text that matches the Layout_Name field I want, it works fine.
I apologize if I have been too wordy, but I figure more info is better than answering a bunch of questions because I forgot something!
TIA!
I am struggling to understand your question. I hope I am addressing the correct issue.
You cannot use Filemaker functions inside an SQL query. To find records where the Layout_Name field is equal to the current layout's name, your query should be:
SELECT Category_List
FROM Categories_VL
WHERE Layout_Name = ?
and then you would supply the current layout's name as an argument:
ExecuteSQL ( "SELECT Category_List FROM Categories_VL WHERE Layout_Name = ?" ; "" ; "" ; Get ( LayoutName ) )
Note that instead of substituting the default delimiter, you can specify your own field and row separators within the ExecuteSQL() function itself, for example:
ExecuteSQL ( "SELECT Category_List FROM Categories_VL WHERE Layout_Name = ?" ; $$delim ; "" ; Get ( LayoutName ) )
See: https://help.claris.com/en/pro-help/#page/FMP_Help%2Fexecutesql.html%23

only taking certain values from a list in perl

First I will describe what I have, then the problem.
I have a text file that is structured as such
----------- Start of file-----
<!-->
name,name2,ignore,name4,jojobjim,name3,name6,name9,pop
-->
<csv counter="1">
1,2,3,1,6,8,2,8,2,
2,6,5,1,5,8,7,7,9,
1,4,3,1,2,8,9,3,4,
4,1,6,1,5,6,5,2,9
</csv>
-------- END OF FILE-----------
I also have a perl program that has a map:
my %column_mapping = (
"name" => 'name',
"name1" => 'name_1',
"name2" => 'name_2',
"name3" => 'name_3',
"name4" => 'name_4',
"name5" => 'name_5',
"name6" => 'name_6',
"name7" => 'name_7',
"name9" => 'name_9',
)
My dynamic insert statement (assume I connected to database proper, and headers is my array of header names, such as test1, test2, ect)
my $sql = sprintf 'INSERT INTO tablename ( %s ) VALUES ( %s )',
join( ',', map { $column_mapping{$_} } #headers ),
join( ',', ('?') x scalar #headers );
my $sth = $dbh->prepare($sql);
Now for the problem I am actually having:
I need a way to only do an insert on the headers and for the values that are in the map.
In the data file given as an exmaple, there are several names that are not in the map, is there a way I can ignore them and the numbers associated with them in the csv section?
basically to make a subset csv, to turn it into:
name,name2,name4,name3,name6,name9,
1,2,1,8,2,8,
2,6,1,8,7,7,
1,4,1,8,9,3,
4,1,1,6,5,2,
so that my insert statment will only insert the ones in the map. The data file is always different, and are not in same order, and an unknown amount will be in the map.
Ideally a efficient way to do this, since this script will be going through thousands of files, and each files behind millions of lines of the csv with hundreds of columns.
It is just a text file being read though, not a csv, not sure if csv libraries can work in this scenario or not.
You would typically put the set of valid indices in a list and use array slices after that.
#valid = grep { defined($column_mapping{ $headers[$_] }) } 0 .. $#headers;
...
my $sql = sprintf 'INSERT INTO tablename ( %s ) VALUES ( %s )',
join( ',', map { $column_mapping{$_} } #headers[#valid] ),
join( ',', ('?') x scalar #valid);
my $sth = $dbh->prepare($sql);
...
my #row = split /,/, <INPUT>;
$sth->execute( #row[#valid] );
...
Because this is about four different questions in one, I'm going to take a higher level approach to the broad set of problems and leave the programming details to you (or you can ask new questions about the details).
I would get the data format changed as quickly as possible. Mixing CSV columns into an XML file is bizarre and inefficient, as I'm sure you're aware. Use a CSV file for bulk data. Use an XML file for complicated metadata.
Having the headers be an XML comment is worse, now you're parsing comments; comments are supposed to be ignored. If you must retain the mixed XML/CSV format put the headers into a proper XML tag. Otherwise what's the point of using XML?
Since you're going to be parsing a large file, use an XML SAX parser. Unlike a more traditional DOM parser which must parse the whole document before doing anything, a SAX parser will process it as it reads the file. This will save a lot of memory. I leave SAX processing as an exercise, start with XML::SAX::Intro.
Within the SAX parser, extract the data from the <csv> and use a CSV parser on that. Text::CSV_XS is a good choice. It is efficient and has solved all the problems of parsing CSV data you are likely to run into.
When you finally have it down to a Text::CSV_XS object, call getline_hr in a loop to get the rows as hashes, apply your mapping, and insert into your database. #mob's solution is fine, but I would go with SQL::Abstract to generate the SQL rather than doing it by hand. This will protect against both SQL injection attacks as well as more mundane things like the headers containing SQL meta characters and reserved words.
It's important to separate the processing of the parsed data from the parsing of the data. I'm quite sure that hideous data format will change, either for the worse or the better, and you don't want to tie the code to it.

Perl "else" statement not executing

I use an ActivePerl script to take in CSV files and create XML files that I load into a database. These are userid database entries, name, address, etc. We've always used the home phone number field to generate an initial password (which we encourage the users to change immediately!). The proliferation of cellphones means I have a bunch of people with no home phone, so I want to use the cell phone field when the home phone field is empty.
My input fields look like this:
# 0 Firstname
# 1 Lastname
# 2 VP (voicepart)
# 3 Address
# 4 City
# 5 State
# 6 Zip
# 7 Phone
# 8 Mobile
# 9 Email
Here's the Perl code I've worked up to create the password - the create_password subroutine is working when there's a value in field 7:
my $pass_word = '';
my $pass_word = create_password($fields[7]);
if (my $pass_word = '') {
print "Use the cell phone number \n";
my $pass_word = create_password($fields[8]);
}
The "print" statement is to tell me what it thinks it's doing.
This looks to me like it should work, but the "if" statment never fires. The Print statement doesn't print, and nobody with a value only in field 8 ever gets a password generated. There must be something wrong with the way I'm testing the value of $pass_word but I can't see it. Should I be testing the values of $fields[7] and $fields[8] instead of the variable value? How DO you test a Perl variable for null value if this doesn't work?
You have several problems in your code.
First of all, after you declared a variable using my, you don't need to add my before the variable when you use it;
Secondly, for this line:
if (my $pass_word = '')
I think you meant
if ($pass_word == '')
(my is removed, as talked in the first point)
= means assignment, which returns the value you assigned to $pass_word, which is '' here, that's why this condition always return false.
But still, == is not correct here. In perl, we use eq to compare two strings. == is used to compare numbers.
So, remove all the my except the first one, and use eq to compare your strings.
You've got two major problems in here.
First one is your string equality test. In Perl, strings are compared for equality using operator eq (as in $string eq 'something'). = is the assignment operator.
Second one is your (ab)use of my. Each my declares a new variable that “hides” the previous one, so in effect you can never re-use its value, you're confronted to undef every time.
Replace = with eq in your if clause; remove all but the first uses of my, and you should be set!
my declares a new variable which hides the variable with the same name in the surrounding scope. Remove the excessive use of my.

Escaping & in perl DB queries

I need to manipulate some records in a DB and write their values to another table. Some of these values have an '&' in the string, i.e. 'Me & You'. Short of finding all of these values and placing a \ before any &'s, how can insert these values into a table w/o oracle choking on the &?
Use placeholders. Instead of putting '$who' in your SQL statement, prepare with a ? there instead, and then either bind $who, or execute with $who as the appropriate argument.
my $sth = $dbh->prepare_cached('INSERT INTO FOO (WHO) VALUES (?)');
$sth->bind_param(1, $who);
my $rc = $sth->execute();
This is safer and faster than trying to do it yourself. (There is a "quote" method in DBI, but this is better than that.)
This is definitely a wheel you don't need to reinvent. If you are using DBI, don't escape the input; use placeholders.
Example:
my $string = "database 'text' with &special& %characters%";
my $sth = $dbh->prepare("UPDATE some_table SET some_column=?
WHERE some_other_column=42");
$sth->execute($string);
The DBD::Oracle module (and all the other DBD::xxxxx modules) have undergone extensive testing and real world use. Let it worry about how to get your text inserted into the database.

Perl DBI: how to see failed query with bound values?

This is a standard inserting example from DBI manual:
my $query = q{
INSERT INTO sales (product_code, qty, price) VALUES (?, ?, ?)
};
my $sth = $dbh->prepare($query) or die $dbh->errstr;
while (<>) {
chomp;
my ($product_code, $qty, $price) = split /,/;
$sth->execute($product_code, $qty, $price) or die ($query . " " . $dbh->errstr);
}
$dbh->commit or die $dbh->errstr;
I modified it a bit, so I can see on die which query failed (die ($query . " " . $dbh->errstr)). Still I'd liked to see the query with bound values (as it was executed). How to get it?
Edit
Btw, i found a awkward way to see query with bound values too: you have to make syntax error in query. For example, if i change query above like that:
my $query = q{
xINSERT INTO sales (product_code, qty, price) VALUES (?, ?, ?)
};
I got it back as i wanted:
DBD::mysql::st execute failed: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'xINSERT INTO sample (product_code, qty, price) VALUES ('1', '2', '3')' at line 1
Sometimes it really helps. At least it did to me.
You can use DBI's ParamValues to obtain the parameter values but you are unlikely to find any method in a DBD to obtain the parameters actually in the SQL because they are mostly sent to the database after the SQL is parsed. You can look at DBIx::Log4perl to see how ParamValues is used in the error handler. You might also find some parts of DBIx::Log4perl useful.
There isn't a standard way to do that. The nearest approximation would be to substitute each placeholder by the (probably quoted) value. In general, it is fairly hard to reliably parse an SQL statement to find the question marks that are placeholders rather than parts of delimited identifiers or strings or comments. In the example, you can simply look for question marks; that won't always work, though:
INSERT /* ? */ INTO "??".Sales VALUES('?', ?, ?, ?);
Note that with most, but not necessarily all, DBMS (and hence most DBD drivers), the statement is sent to the DBMS when it is prepared; only the values are sent when the statement is executed. There never is a statement created with all the values substituted into the VALUES list, so neither DBI nor the DBMS should be expected to create one.
There isn't any generically supported DBI way to do this, I think, but individual database drivers might allow it. What database are you using?