Setting Linq2Db to read and update with CHAR type and NOT NULL constraint in Oracle - oracle

Reading value of fixed CHAR string from table, using Linq2db Oracle provider:
CREATE TABLE mytable
(pk NUMBER(15,0) NOT NULL,
fixed_data CHAR(20) DEFAULT ' ' NOT NULL)
Although in database, length of FIXED_DATA filed is 20,
SELECT LENGTH(fixed_data) FROM mytable WHERE pk = 1
-- result is 20
When same field is read using Linq2Db, value gets truncated to empty string:
var row = (from row in database.mytable where row.pk == 1 select row).ToList()[0];
Console.WriteLine(row.fixed_data.Length);
// result is zero
This causes problem when record is updated using Linq2Db, Oracle converts empty string to NULL, and UPDATE fails:
database.Update(row);
// Oracle.ManagedDataAccess.Client.OracleException: 'ORA-01407: cannot update ("MYSCHEMA"."MYTABLE"."FIXED_DATA") to NULL
Is there any setting in Linq2Db for read->update cycle to work with CHAR type and NOT NULL constraint?

Found a solution, thanks to source code openly available. By default, Linq2Db calls expression IDataReader.GetString(int).TrimEnd(' ') on every CHAR and NCHAR column. However this can be easily customized, by implementing custom provider, which overrides field value retrieval expression, with one that does not trim:
class MyOracleProvider : OracleDataProvider
{
public MyOracleProvider(string name)
: base(name)
{
// original is SetCharField("Char", (r,i) => r.GetString(i).TrimEnd(' '));
SetCharField("Char", (r, i) => r.GetString(i));
// original is SetCharField("NChar", (r,i) => r.GetString(i).TrimEnd(' '));
SetCharField("NChar", (r, i) => r.GetString(i));
}
}

Related

Return auto increment value in Oracle SQL Insert statement?

I have Insert statement that should return Auto Increment column value. Here is example of my insert:
myQuery = new query();
myQuery.name = "insertRec";
myQuery.setDatasource("db");
mySQL = "
INSERT INTO myTable (
First, Last, Email, ActionDate
)VALUES(
:first, :last, :email, :actiondt
)
";
myQuery.setSQL(mySQL);
myQuery.addParam(name="first", cfsqltype="cf_sql_varchar", value="#trim(form.first)#", maxlength="50");
myQuery.addParam(name="last", cfsqltype="cf_sql_varchar", value="#trim(form.last)#", maxlength="50");
myQuery.addParam(name="email", cfsqltype="cf_sql_varchar", value="#trim(form.email)#", maxlength="320");
myQuery.addParam(name="actiondt", cfsqltype="cf_sql_timestamp", value="#now()#");
myQuery.execute().getResult();
//result = myQuery.execute().getResult(); This line is commented because I was getting error message that test variable is not defined. Not sure why since test is declared and equals to myQuery.execute().getResult();
After I run this code record will appear in the table. Result set looks like this:
RecordID First Last Email ActionDt
7 John Woss jwoss#gmail.com 16-NOV-18
As you can see RecordID (auto increments) is there. I would like to get that value once myQuery is completed. How to achieve that?
As outlined in https://stackoverflow.com/a/34894454/432681, you can retrieve the auto-incremented ID via a SELECT on the related sequence.
So, your query should look something like this:
mySQL = "
INSERT INTO myTable (
First, Last, Email, ActionDate
)VALUES(
:first, :last, :email, :actiondt
);
SELECT "sequenceForMyTable".currval from dual;
";
How to get the name of the sequence is also described in the linked answer above.
There's a second way outlined in https://stackoverflow.com/a/25268870/432681 and also mentioned in the CFML documentation, which says that you may use the getPrefix() function to be able to access the generated key. Note that in this case result is the object returned by the execute() method, not the getResult() (because the query doesn't return a result set). So for your example this is how you can get the ID:
result = myQuery.execute();
newID = result.getPrefix().generatedKey;

Bltoolkit oracle clob type

I use bltoolkit as orm and i had a problem with clob type.
I have a long string value and i got error while update operation.
Error: ORA01704 - String literal too long.
Checked table and my column type is clob.
There is no clob option in bltoolkit table class design.
I set this column like that:
[MapField("MSG_BODY")]
public string MsgBody { get; set; }
What is wrong ?
I find a solution, post only clob columns and it works !
//update only body
value = db.Schedule
.Where(x => x.Rowversion == _zaman
&& x.ScheduleId == this.ScheduleId)
.Set(x => x.Rowversion, x => _zaman)
.Set(x => x.MsgBody, x => this.MsgBody)
.Update();

Dashes Causing SQL Trouble in DBI

I have a SQL query with a WHERE clause that typically has values including a dash as stored in the database as a CHAR(10). When I explicitly call it like in the following:
$sth = $dbh->prepare("SELECT STATUS_CODE FROM MyTable WHERE ACC_TYPE = 'A-50C'");
It works and properly returns my 1 row; however if I do the following:
my $code = 'A-50C';
$sth = $dbh->prepare("SELECT STATUS_CODE FROM MyTable WHERE ACC_TYPE = ?");
$sth->execute($code);
or I do:
my $code = 'A-50C';
$sth = $dbh->prepare("SELECT STATUS_CODE FROM MyTable WHERE ACC_TYPE = ?");
$sth->bind_param(1, $code);
$sth->execute();
The query completes, but I get no results. I suspect it has to do with the dash being interpretted incorrectly, but I can't link it to a Perl issue as I have printed my $code variable using print "My Content: $code\n"; so I can confirm its not being strangely converted. I also tried including a third value for bind_param and if I specify something like ORA_VARCHAR2, SQL_VARCHAR (tried all possibilities) I still get no results. If I change it to the long form i.e. { TYPE => SQL_VARCHAR } it gives me an error of
DBI::st=HASH<0x232a210>->bind_param(...): attribute parameter
'SQL_VARCHAR' is not a hash ref
Lastly, I tried single and double quotes in different ways as well as back ticks to escape the values, but nothing got me the 1 row, only 0. Any ideas? Haven't found anything in documentation or searching. This is oracle for reference.
Code with error checking:
my $dbh = DBI->connect($dsn, $user, $pw, {PrintError => 0, RaiseError => 0})
or die "$DBI::errstr\n";
# my $dbh = DBI->connect(); # connect
my $code = 'A-50C';
print "My Content: $code\n";
$sth = $dbh->prepare( "SELECT COUNT(*) FROM MyTable WHERE CODE = ?" )
or die "Can't prepare SQL statement: $DBI::errstr\n";
$sth->bind_param(1, $code);
$sth->execute() or die "Can't execute SQL statement: $DBI::errstr\n";
my $outfile = 'output.txt';
open OUTFILE, '>', $outfile or die "Unable to open $outfile: $!";
while(my #re = $sth->fetchrow_array) {
print OUTFILE #re,"\n";
}
warn "Data fetching terminated early by error: $DBI::errstr\n"
if $DBI::err;
close OUTFILE;
$sth->finish();
$dbh->disconnect();
I ran a trace and got back:
-> bind_param for DBD::Oracle::st (DBI::st=HASH(0x22fbcc0)~0x3bcf48 2 'A-50C' HASH(0x22fbac8)) thr#3b66c8
dbd_bind_ph(1): bind :p2 <== 'A-50C' (type 0 (DEFAULT (varchar)), attribs: HASH(0x22fbac8))
dbd_rebind_ph_char() (1): bind :p2 <== 'A-50C' (size 5/16/0, ptype 4(VARCHAR), otype 1 )
dbd_rebind_ph_char() (2): bind :p2 <== ''A-50' (size 5/16, otype 1(VARCHAR), indp 0, at_exec 1)
bind :p2 as ftype 1 (VARCHAR)
dbd_rebind_ph(): bind :p2 <== 'A-50C' (in, not-utf8, csid 178->0->178, ftype 1 (VARCHAR), csform 0(0)->0(0), maxlen 16, maxdata_size 0)
Your problem is likely a result of comparing CHAR and VARCHAR data together.
The CHAR data type is notorious (and should be avoided), because it stores data in fixed-length format. It should never be used for holding varying-length data. In your case, data stored in the ACC_TYPE column will always take up 10 characters of storage. When you store a value whose length is less than the size of the column, like A-50C, the database will implicitly pad the string up to 10 characters, so the actual value stored becomes A-50C_____ (where _ represents a whitespace).
Your first query works because when you use a hard-code literal, Oracle will automatically right-pad the value for you (A-50C -> A-50C_____). However, in your second query where you use bind variables, you're comparing a VARCHAR against a CHAR and no auto-padding will happen.
As a quick fix to the problem, you could add right-padding to the query:
SELECT STATUS_CODE FROM MyTable WHERE ACC_TYPE = rpad(?, 10)
A long-term solution would be to avoid using the CHAR data type in your table definitions and switch to VARCHAR2 instead.
As your DBI_TRACE revealed, the ACC_TYPE column is CHAR(10) but the bound parameter is understood as VARCHAR.
When comparing CHAR, NCHAR, or literal strings to one another, trailing blanks are effectively ignored. (Remember, CHAR(10) means that ACC_TYPE values are padded to 10 characters long.) Thus, 'A ' and 'A', as CHARs, compare equal. When comparing with the VARCHAR family, however, trailing blanks become significant, and 'A ' no longer equals 'A' if one is a VARCHAR variant.
You can confirm in sqlplus or via quick DBI query:
SELECT COUNT(1) FROM DUAL WHERE CAST('A' AS CHAR(2)) = 'A'; -- or CAST AS CHAR(whatever)
SELECT COUNT(1) FROM DUAL WHERE CAST('A' AS CHAR(2)) = CAST('A' AS VARCHAR(1));
(Oracle terms these blank-padded and nonpadded comparison semantics, since the actual behavior per the ANSI-92 spec is to pad the shorter CHAR or literal to the length of the longer and then compare. The effective behavior, whatever the name, is that one ignores trailing blanks and the other does not.)
As #MickMnemonic suggested, RPAD()ing the bound value will work, or, better, altering the column type to VARCHAR. You could also CAST(? AS CHAR(10)). TRIM(TRAILING FROM ACC_TYPE) would work, too, at the cost of ignoring any index on that column.

Selecting last record by linq fails with "method not recognised"

i have following query to select the last record in the database
using (Entities ent = new Entities())
{
Loaction last = (from x in ent.Locations select x).Last();
Location add = new Location();
add.id = last.id+1;
//some more stuff
}
the following error is returned when calling the method containing these lines via "Direct event" on ext.net:
LINQ to Entities does not recognize the method 'Prototype.DataAccess.Location Last[Location]
(System.Linq.IQueryable`1[Prototype.DataAccess.Location])' method,
and this method cannot be translated into a store expression.
table structure is following:
int ident IDENTITY NOT NULL,
int id NOT NULL,
varchar(50) name NULL,
//some other varchar fields
Last is not supported by Linq to Entities. Do OrderByDescending (usually by ID or some date column) and select first. Something like:
Location last = (from l in ent.Locations
orderby l.ID descending
select l).First();
Or
Location last = ent.Locations.OrderByDescending(l => l.ID).First();
See MSDN article Supported and Unsupported LINQ Methods (LINQ to Entities) for reference.

Linq 2 SQL Sum using lambda and handling nulls

When using sum with lambda in Linq to SQL using the following code:
int query = (from f in odc.RDetails
where f.ticketID == int.Parse(ticket.ToString())
select f).Sum(x => x.Rate);
I get the following error:
The null value cannot be assigned to a member with type System.Int32 which is a non-nullable value type.
. You have to make sure x.Rate is an int, and not an int? (an int that accepts null as a value).
. If the query has no elements, .Sum won't do anything and will return null. Choose a default value, let's say 0.
var query = from f in odc.RDetails
where f.ticketID == int.Parse(ticket.ToString())
select f;
int result = query.Any()
? query.Sum(x => x.Rate ?? 0) // use the ?? if x.Rate is an "int?".
: 0; // default value you can choose.
I would break the int.Parse(ticket.ToString()) onto its own line to isolate that parse from the Linq for debugging.
We don't know whether that is throwing the exception or if one of the RDetails.Rate values is null. Is it indeed a Nullable<int>?
If RDetails.Rate is a Nullable<int>, then you could ...Sum(x => x.Rate ?? 0) and avoid the exception.

Resources