Sqlite database reference to text box

@MOULTC15A,

I hope I am not too late in “chipping” in. I read the thread. However, I am not sure if I have fully understood what you are trying to achieve in your “Financial Manager App”. I have made one working app (code below), roughly following what I think you want to do, and making a few assumptions. The assumptions and considerations are,

  • The “income” scene is used to receive repeat data entry of “income”.
  • You can use the “done” or “enter” or “go” button on the Windows keyboard or virtual Android keyboard or,
  • You can also use the “done” button to complete the entry. Hence, there are two event listeners, to cater for each. Please see insertIncome() and textListener().
  • It is more efficient for this nature of app to open and close the db once. Please see initDB() and closeDB() and their placement.
  • I also use an external program to open the db and double-check the integrity of the data entered in sqlite. The program I am using on my Windows is “db browser”. Here is the URL,
    https://sqlitebrowser.org/

The code below may not be 100% exactly what you want to do. But please feel free to modify it and update it for your exact usage.

Here is the working code,

display.setDefault( "background", 0.8 )
local composer = require( "composer" )
local sqlite3 = require( "sqlite3" )
local scene = composer.newScene()
--    Forward declare display objects, functions and etc 
local db, text, numericField, textEntered
local initDB, closeDB 
--    Forward declare dimensions and other "constants"
local width = display.contentWidth
local height = display.contentHeight
local centerX = display.contentCenterX
local centerY = display.contentCenterY
local headerHeight = height * 0.2
local labelWidth = width * 0.2
-- ------------------------------------------------------------------------------
function initDB()
    local path = system.pathForFile( "data.db", system.DocumentsDirectory )
    if ( not db or not db:isopen() ) then 
        db = sqlite3.open( path )
    end
    --    Creates the table for income, 'income' is the table name, 'IncomeValue' is the column
    local incomeTableSetup = [[CREATE TABLE IF NOT EXISTS income ( IncomeValue INTEGER PRIMARY KEY autoincrement );]]
    db:exec( incomeTableSetup )
end
function closeDB()
   if ( db and db:isopen() ) then
        db:close()
        db = nil 
    end
end 
function insertRecord( text )
	if text ~= nil and text ~= "" then 
		local integer = tonumber( text )
		--	More check on input value can be performed here, if integer is equal to 0 
		local insertQuery = [[INSERT INTO income VALUES ( ']] .. integer .. [[' );]]
		db:exec( insertQuery )
		--	Show the entry 
		enteredText.text = text
		--	clear the text field for next input 
		numericField.text = ""
		text = nil 
	end 
end 
--    When the value is written in the text box, then the done button is pressed, this function runs
local function insertIncome( event )
    if event.phase == "ended" or event.phase == "submitted" then
        print( event.name, event.phase, "insert income done:", tostring( text ) )
        insertRecord( text )
    end 
end
--    When user editing or submitted the input via native text field 
local function textListener( event )
    if ( event.phase == "began" ) then
        event.target.text = ""
    elseif ( event.phase == "ended" or event.phase == "submitted" ) then
        -- Output resulting text from "defaultField"
        print( event.name, event.phase, "input from native text field:", event.target.text )
        text = event.target.text 
        insertRecord( text )
    elseif ( event.phase == "editing" ) then
        text = event.newCharacters
    end
end
--      If the menu button is pressed
local function gotoMenu()
    composer.gotoScene( "menu" )
end
--      create()
function scene:create( event )
    local sceneGroup = self.view
    initDB()
    local banner = display.newRect( sceneGroup, centerX, headerHeight * 0.5, width, headerHeight )
    banner:setFillColor( 0, 0, 1 )
    local incomeBanner = display.newText( sceneGroup, "Income: Data Entry", centerX, headerHeight * 0.5, native.systemFont, headerHeight * 0.2 )
    incomeBanner:setFillColor( 1, 1, 1 )
    local menuButton = display.newRect( sceneGroup, width * 0.2, height * 0.8, labelWidth, headerHeight * 0.5 )
    menuButton:setFillColor( 0, 1, 0 )
    menuButton:addEventListener( "tap", gotoMenu )
    local menuText = display.newText( sceneGroup, "Menu", width * 0.2, height * 0.8, native.systemFont, headerHeight * 0.2 )
    menuText:setFillColor( 0, 0, 0 )
    local amountText = display.newText( sceneGroup, "Add Amount:", centerX, height * 0.3, native.systemFont, headerHeight * 0.2 )
    amountText:setFillColor( 0, 0, 0 )
    local doneButton = display.newRect( sceneGroup, centerX, height * 0.5, labelWidth, headerHeight * 0.5 )
    doneButton:setFillColor( 0, 1, 0 )
    doneButton:addEventListener( "touch", insertIncome )
    local doneText = display.newText( sceneGroup, "Done", centerX, height * 0.5, native.systemFont, headerHeight * 0.2 )
    doneText:setFillColor( 0, 0, 0 )
    enteredText = display.newText( sceneGroup, "Entered Text", centerX, height * 0.8, native.systemFont, headerHeight * 0.2 )
    enteredText:setFillColor( 0, 0, 0 )
    numericField = native.newTextField( centerX, height * 0.4, labelWidth * 4, headerHeight * 0.4 )
    numericField.inputType = "number"
    numericField.placeholder = "enter income here"
    numericField:addEventListener( "userInput", textListener )
    sceneGroup:insert( numericField )
end
--      show()
function scene:show( event )
    local sceneGroup = self.view
    local phase = event.phase
    if ( phase == "will" ) then
    elseif ( phase == "did" ) then
    end
 end
 --      hide()
function scene:hide( event )
     local sceneGroup = self.view
     local phase = event.phase
     if ( phase == "will" ) then
     elseif( phase == "did" ) then
        display.remove( numericField )
        closeDB()
        composer.removeScene( "income" )
    end
end
scene:addEventListener( "create", scene)
scene:addEventListener( "show", scene)
scene:addEventListener( "hide", scene)
scene:addEventListener( "destroy", scene)
return scene
1 Like

Thanks for “chipping” in, sorry to be a hassle (i’m still pretty new with this stuff). I will try to implement this code into another project to see how it works. The idea you have made is pretty close to what I was aiming to do. Overall, I had a menu screen which will show a ‘summary’, with week, month and to date income, same with expenses. I chose sqlite as the db method as I knew I would need to place individual values into a db and then use that information later (e.g. in the summary/menu). I’m assuming there is a way to add up different values in the db. What I am not sure of however is if these values can be timed when they are entered.
For example if the menu is to show ‘week income’, the program would have to know the day in which the values are entered in the db and add the values from the last week. I’m not sure if this is possible, but I just wanted to get this first step out the way before looking at that.

@MOULTC15A,
After your further explanation, I would agree that you made the right choice of using “sqlite”. “sqlite” is certainly more efficient to do aggregate sum and aggregate average, by month, by week and etc, than using “json”. What you described is certainly very doable in “sqlite” and with Solar2D.

So, when you create the table “income”, you want to add in another column called “created” or “timestamp”, which should store the time association with the entered income value. There are multiple ways you want to capture this. For example,

  • You could create additional “date” input for manual user input using “widget” to select or,
  • manual user input using “textField” to enter or,
  • If it make sense, auto-create present date-time when the income value is entered (via os.time() ).

This date or date-time should be added together with the income value, when you do “insertRecord()”.

I hope you get the idea. As you have said that you would take one step at a time, to add in new functionalities later. I would agree with this approach. Wish you all the best!

PS. A minor “edit” needed to above code. Under the insertIncome() function, there should not be an event phase return called “submitted”, as this is a function to handle “touch” event. It can be removed without impact to the function. I probably missed it out when I copy and paste.

1 Like

Hey,
So with the external program, this will show all the pieces of data entered into the db? Because the console currently only shows the most recent value entered.

All good I understand now, I have downloaded the program and found the db. I created two different db’s for income and expense, so I’ll start making some edits so I can start manipulating the data. Can the external browser do any edits itself? Or is it a view only kind of thing?

The various “system.xxxDirectory” are in respective app folders within the sandbox. I am using Solar2D Simulator on Windows (SOW). The sandbox for the simulator for me is at (where user-id is your Windows user id),

C:\Users\<user-id>\AppData\Local\Corona Labs\Corona Simulator\Sandbox\

While you are exploring the sandbox, you should also have a quick look at the following. It may be useful for you in the future.

C:\Users\<user-id>\AppData\Roaming\Corona Labs\Corona Simulator\
C:\Users\<user-id>\AppData\Roaming\Solar2DPlugins\Caches\

Good that you found it on your own. Didn’t see your most recent post, as I was composing and writing my response.

Yes. You can use the “db browser” to edit the db. You can also literally manipulate most part of db. You can check out the docs on their website for details.

Just a suggestion. Instead of using two dbs, you can consider using two tables in one db, one table for “income” and another table for “expense”. It may be more efficient this way.

Yep all good,
Having both income and expenses in the same db is a better idea yes, It should make it easier to calculate balances as well (minus one from the other), I don’t know why I didn’t do that!
Thanks, I will be sure to reply in the forum if I happen to encounter any other issues.

Hi there,
Think I can help so will make some suggestions:

  • Don’t use IncomeValue as your primary key. This should be unique, and you’re likely to enter the same income multiple times. Just use a new column called “PK” for example - let it auto increment so each record in the db has unique identifier.
  • Definitely add a date column as suggested by @luantiang. Either auto generate the date through a function or get the user to enter it. They might have purchased something yesterday and want to add it today so consider letting the user decide.
  • Consider adding income and expenses to the same table. Have different fields (columns) for each - so create and populate Credit for income and Debit for expenses. This will make your life easier when you come to write queries for totals over set periods.
  • Don’t be afraid to add some other columns which you can use later, for example a Category and Description so you can create/display some additional stats.

Hope this helps,
Nick

Hey Nick,
Thanks for the recommendations, I will keep this in mind. I was planning on having a function which automatically timestamped, but your suggestion seems like a good idea, as people can sometimes purchase something on a different day and enter it later.

1 Like

You are very welcome.
Financial databases is my day job - and yes this happens all the time. You could also have both - time stamp of the entry and date the user wants to report the income or expense. Suggest you use sensible, descriptive names if you do so as it can easily become confusing…!

Hey again,
so I have added a date column next to the PK column (which is where values are inputted). I have also written a command which displays the income for the week. However, I wasn’t sure how to create a function which can first of all add values in the database, but also check the date. I’m assuming it would have to be something like

if "timeStamp" <= "A week ago" then
   display (value from row.PK + value from row.PK until == A week ago)

Currently I have a simple command:

for row in db:nrows( "SELECT * FROM income" ) do
 local weekIncomeText = row.PK 
 local t = display.newText( weekIncomeText, 200, 2*row.PK, nil, 16)
 t:setFillColor( 0, 0, 0 )
 print( "Row:", row.PK )
end

However this will only display all the values in the row, in weird locations vertically along the screen.
I will try to look around for any tutorials on how to add values in a database, but any suggestions would be nice
Thanks!

Sorry for the delay in replying…!
Difficult without seeing the data, but you’re using the primary key - PK - as the y coordinate for your text, which is why it’s displayed in “weird” locations down the screen.
As mentioned above - best to leave PK as simply an integer, then have a Value field which holds the expenses/income.
Also not sure why you’d want to change the location of your text (i.e. using it as the y coordinate) depending on the amount - would guess they are listed at static points on the screen.
For the actual query for income for the week it would be something like:

SELECT SUM(Value) FROM income WHERE date > “13 Sep 2020” AND date < “21 Sep 2020”

You could obviously use variables for the dates and populate them depending on what week you desire.

For example:

local startDate, endDate, weeklyIncome = “13 Sep 2020”, “21 Sep 2020”
local function getData(udata, cols, values, names)
	weeklyIncome = values[1]
	return 0
end

db:exec([[SELECT SUM(Value) FROM income WHERE date > "]]..startDate..[[" AND date < "]]..endDate..[["]], getData)

Forums amaze me… Not being rude but people creating “finance apps” yet struggling to grasp the concept of a primary key?

1 Like

@anon63346430 being real with you here,
this is purely for a school project not necessarily something I’d actually plan to implement. I decided to use databases and thought that a financial management app would fit well with a database project.
As such, I am still very inexperienced with lua programming, especially sqlite. (which means I don’t fully understand the concepts of many things around sqlite).

OK didn’t realise you was at school… Maybe mention that in future.
.

1 Like

@DigiNick So ‘PK’ will simply be labelled as 1,2,3 etc. every time a value is entered in the database? I changed the line of code in the income scene so when values are being inserted PK is written as NULL, then integer, then the date e.g.
[[INSERT INTO income VALUES ( NULL, ']] .. integer .. [[',']].. os.date("!%Y-%m-%d") ..[[' );]]

I have since changed the positioning of the text. Unsure why I chose pk as the y-coordinate. (might have had something to do with experimentation from a documentation page)

I’ve edited the query, so it looks like this currently:

local seven_days_ago = os.date( "!%Y-%m-%d", os.time() - 24*60*60*7 )
local now = os.date( "!%Y-%m-%d" )
local function getData()
   weekIncome = values[1]
   return 0
end
db:exec([[SELECT * SUM(incomeValue) FROM income WHERE timeStamp > "]]..seven_days_ago..[[" AND timeStamp < "]]..now..[["]], getData)
print(weekIncome) 

However, the console prints nil. Better than an error I suppose! I was unsure what the statements inside the getData brackets meant (udata, cols, values, names) so I left it out.

No problem, sorry I didn’t mention it at the beginning.

Yes PK will simply be 1,2,3, etc.
The idea of a primary key is that it’s a unique identifier for a record - you can’t use your amount for this as it’ll be repeated sooner or later - likely sooner in this instance…!
You need those values (parameters) in the getData function - include all 4.
The one we want though is values - that’s the information in the column(s) we’re returning from the database.
values[1] means the first value - your query will only return one as we’re summing the incomeValues into one amount.
Leave the star out of your select too - you only want the sum of incomeValues for this particular query.

local seven_days_ago = os.date( "!%Y-%m-%d", os.time() - 24*60*60*7 )
local now = os.date( "!%Y-%m-%d" )
local function getData(udata, cols, values, names)
   weekIncome = values[1]
   return 0
end
db:exec([[SELECT SUM(incomeValue) FROM income WHERE timeStamp > "]]..seven_days_ago..[[" AND timeStamp < "]]..now..[["]], getData)
print(weekIncome)

Okay that makes sense,
I made the changes you mentioned, however the console is still outputting nil. I have two inputted values in the database currently with (10 and 50). So if it works, it should output 60.