Use table-object as a part of SQL Query
During runtime I build an object which is essentially a wrapper over in-memory table:
col1 | ... | colN |
val11 | ... | valN1 |
val12 | ... | valN2 |
I want to use this object as a part of INSERT or UPDATE queries, based on a value of some column (the main use case one of the columns is an ID value)
What's the best way to expose the object to SQL?
I have complete control over object class, so if I need to add something (or even change runtime internals), it's no problem.
Object may have different columns depending on runtime situation, but during one query execution the fields are constant.
I'm interested in the most performant solution.
I want to write queries like this
INSERT INTO table (Name,Age) SELECT Name,Age FROM myObject
And:
UPDATE table AS t
SET t.Name = obj.Name
FROM myObject AS obj
WHERE t.Id = obj.Id
Create a temp table with all properties you need, store it in a Cachte/IRIS-Temp unde $J of the running job (it could be, your applcation runs in several instances at the same time) an use it in your INSERT / UPDATE.
This is your Table
Class My.Table1 Extends %Persistent { Property Name As %String; Property Age As %Numeric; ClassMethod Test() { d ##class(My.Temp).%DeleteId($j) s tmp=##class(My.Temp).%New() s tmp.TempID=$j, tmp.Name="Paul", tmp.Age=69 d tmp.%Save() // or popolate the My.Temp via INSERT... &sql(INSERT INTO My.Table1 (Name,Age) SELECT Name,Age FROM My.Temp WHERE TempID=$j ) } }
and this is the Temporary Table
Class My.Temp Extends %Persistent { Parameter DEFAULTGLOBAL = "^CacheTemp.TempTable"; Property TempID As %Integer; Property Name As %String; Property Age As %Numeric; Index main On TempID [ IdKey ]; }
Interesting! Tank you!
I can also use PPG for runtime storage.
coming late to the party, but just as a small and likely irrelevant note: if you use a PPG for storage and at some point (because the table appears big) the query optimizer thinks it's worth using parallel query execution, it won't see a thing in those worker processes. You should be able to avoid that using the %NOPARALLEL keyword though.
You didn't specify a version so I'll use IRIS 2020.1. This should also work in 2019 versions. My example uses a Table-Valued Function. Any query can potentially be used as a table valued function. %SQL.CustomQuery simply generates all the infrastructure for you. Custom query works quite simply and is very similar to an embedded SQL cursor - you implement Open, Fetch and Close yourself (%OpenCursor, %FetchCursor, %CloseCursor). You define properties that correspond to columns in your row. You define private properties to hold the cursor state. Open typically binds the source data to the instance of your custom query and close releases any resources. Fetch simply advances the cursor to the next row, populating the properties that represent columns in your table. Look at the class doc for %SQL.CustomQuery for more details.
The class is simple:
Thank you for the info, Daniel!
Can I implement %GetProperty instead of specifying the list of properties explicitly?
I don't want to compile a new class for each new table-object.
That is a problem. SQL requires metadata to be provided at statement prepare time. Any SQL-Invokable-Routine (function - including Table-Valued Function - and procedure as invoked by CALL) must provide that metadata and that metadata is generated by the compiler. There is no dynamic prepare-time hook unfortunately.
There is syntax in standard SQL that allows you to provide "value to column binding" within the statement (late-schema binding) but we do not support that.
I suppose I can have one Id column and several col1, col2 ..., colN columns, so losing the names.
You could try this way:
Class My.Table2 Extends %Persistent { Property Name As %String; Property Age As %Numeric; Property City As %String; Property Phone As %String; ClassMethod Test() { // This is your Data-Object... set data=[] do data.%Push({"Name":"Joe", "Age":44, "City":"Boston", "Phone":"1-234-4567"}) do data.%Push({"Name":"Ron", "Age":48, "City":"Dallas", "Phone":"1-234-5678"}) do data.%Push({"Name":"Eve", "Age":40, "City":"Miami", "Phone":"1-234-4567"}) do data.%Push($lb("Tommy", 50, "New York", "1-345-6789")) do data.%Push($lb("Alexa", 35, "Portland", "1-567-8901")) // Now insert all the above data into your table... if 'data.%Size() quit set cnt=0, size=data.%Size() while $i(cnt)<=size { set rowData=data.%Get(cnt-1) &sql( INSERT INTO My.Table2 (Name,Age,City,Phone) VALUES (My.Table2_DataProvider(:rowData), :row(2), :row(3), :row(4)) ) } } ClassMethod DataProvider(rowData) As %String [ PublicList = row, SqlProc ] { kill row if $isobject(rowData) { set it=rowData.%GetIterator() while it.%GetNext(,.val) { set row($i(row))=val } } else { for i=1:1:$ll(rowData) { set row(i)=$lg(rowData,i) } } quit row(1) } Storage Default { <Data name="Table2DefaultData"> <Value name="1"> <Value>%%CLASSNAME</Value> </Value> <Value name="2"> <Value>Name</Value> </Value> <Value name="3"> <Value>Age</Value> </Value> <Value name="4"> <Value>City</Value> </Value> <Value name="5"> <Value>Phone</Value> </Value> </Data> <DataLocation>^My.Table2D</DataLocation> <DefaultData>Table2DefaultData</DefaultData> <IdLocation>^My.Table2D</IdLocation> <IndexLocation>^My.Table2I</IndexLocation> <StreamLocation>^My.Table2S</StreamLocation> <Type>%Library.CacheStorage</Type> } }
Create your data for insert and then
INSERT into yourtable (Prop1, Prop2, ...) VALUES (sqlProcForTheFirstValue(), :localVarForOtherValues(2), :localVatForOtherValues(3),...)
see the above example.
Take care of the sequence of INSERT names and row(i) values.
Or something like that:
Does not work for me:
Thank you for asking.
It seems that no one has tried my code in all this time, or it's just that no one is interested in it.
There are two ways to solve the issue:
Thanks Vitaliy, it works now!
wow. Never knew we have this: Undefined=2.
Thanks, Vitaly!