TweetFollow Us on Twitter

Tightwads Flat File db
Volume Number:12
Issue Number:12
Column Tag:Tools Of The Trade

A Tightwad’s Guide to Flat File Databases

Working with relations in a flat file data base engine

by Edward Ringel, Waterville, Maine

Note: Source code files accompanying article are located on MacTech CD-ROM or source code disks.

By Way of Introduction

One of the most naive, chutzpah-riddled tasks I ever set for myself was trying to build an accounts receivable program for my medical practice in my spare time after reading the Omnis 3 manual and tutorial. After spending a lot of time (and obviously not an inconsiderable sum on this program) I gave up, got wise and bought Tess 2.0, which we’ve used happily through to the present version of 4.4. It is interesting that this fiasco propelled me 1) to program in a language, rather than a database, so that I had complete control of the environment and 2) to think a bit about databases and database design.

Programming to solve database problems using a general purpose computer language inevitably turns one’s attention to database engines and source code libraries to lighten the programming load. While the ads for many of these products promise all kinds of relational muscle, a lot of serious programming tasks don’t need that kind of horsepower. In the course of completing several programming tasks of interest to me, I’ve developed some techniques for using “less powerful” flat file database engines that may be of general interest. For some of you, this may be fairly elementary; for others enlightening. Please bear with me.

Tools and Questions

When I try to solve any problem, I try to conceive the question as carefully as possible and then look at the tools available that might be useful. For the purpose of this discussion, let’s alter the normal thought process and start with the tools. The high priced libraries are expensive for several good reasons.

These database engines are powerful. They can handle thousands, if not millions of records. They can do wonderfully fast searches. They are optimized to read and write keys and data as fast as the OS will permit. They do extensive error checking and error correction. They permit the arbitrary changing of record size, key size, etc. They permit the creation of relational tables and thus the creation of relational databases. Some interface with object oriented paradigms.

Does Your Problem Really Need a Tool This Powerful?

Most of us write databases that handle hundreds or thousands of records, not millions. Most of us write programs where very fast search, read, and write functions are not critical because there is a user sitting there who can’t type that fast. I’m old fashioned about error checking; I don’t feel as though I can ever turn down responsibility for this aspect of programming. Good planning can obviate the need for after-the-fact changes to database structure, and even better planning can allow for it. Relational tables, while useful, are expensive in terms of efficiency and disk space, and besides 1) a lot of really important information in our lives is flat in structure, and 2) the data can be represented and interrelated with a simpler scheme than a relational table. Object persistence is neat, and I don’t know how to write an object persistence scheme; for all I know maybe you can’t come close to NeoAccess or its cousins with what I’m going to propose. Maybe I’m way off base. However, nothing in my thinking precludes a database of objects where constructor/initializer routines reestablish object relationships as the objects are loaded.

Why a Flat File Database Library?

It is time inefficient, error prone, and hard to write a set of routines that manages keys, records, and files. While it may be hard to justify the cost of some of the high end products, it is equally hard to justify the enormous investment of time and hassle in writing your own set of database routines. A flat file data base engine is an excellent compromise between cost and features. This tool is reasonably efficient, and is within most programmers’ budgets. It supplies you with the code that is the most difficult to write. There will still be an investment of time and energy that will be necessary, but it won’t be nearly as dear as starting from scratch. This kind of library has three main features:

• A scheme for record allocation, deletion, and reuse of deleted record file space.

• A record indexing scheme. This is usually some sort of b-tree, but can be a hash table or some other indexing system.

• Extensive error checking during record and key operations, and result codes that indicate 1) whether the database engine did what you asked it to do and 2) if there was an operating system I/O error.

These basic functions can be used very successfully to manage relatively complex data sets. I am going to assume that anyone reading this article understands the basic concepts of record and key operations.

I use a library called B-Tree Helper, from (m)agreeable software in Plymouth, Minnesota (Magreeable@aol.com). The current version is 2.2. This product comes as C source code. The function calls are straightforward. Mel Magree is delightfully old fashioned in that he actually includes a hard copy manual for no extra charge! The product can handle fixed and variable length records, many indices, and is very Mac-like in that a single physical file on disk can contain multiple index and record types; no separate index and record file is needed for each logical grouping of data (unless you wish to do so.)

Some of the earlier incarnations of B-Tree Helper had some clumsy function calls and too many pointers to too many temporary results. The current version is very nice and well worth the upgrade if you are an owner of an older version. This product is used for the demonstration application. The source code provided for the demo will compile only if you supply the .c files to B-Tree Helper; the .h files for the library are provided. B-Tree Helper is a robust, good product and has met my needs nicely. There are other flat file engines available, but this article is not a product review. The reader can easily find and compare different products.

Invoicing Revisited

To make my points, and to demonstrate some techniques, I would like to revisit the model problem of invoicing. This is a standard database problem that some of you may have encountered previously in examples or classes. The classical invoicing structure, at its simplest, contains four different types of records, each contained within its own file. Figure 1 shows the basic interrelationships of data.

Figure 1.

There is a customer, a part, an invoice, and a line item. The customer record contains data about the customer, how much he/she owes, etc. The parts record contains information about each item being sold. The invoice is an open-ended structure that tells us about who the customer is, and the various items sold to the customer at that interaction. Each line item consists of the part, the number of parts sold, the price, etc.

When there is a sale, the program would create an invoice, n number of line items, and relate the invoice and line items to pre-existing records that represented customer and part information. Each line item structure contains a reference to the part information for that line item. This can be done either through a relational table, or it may be done simply by embedding a reference to the part information in the line item. When a line item record is retrieved, the application is written so that the information about the part reference is retrieved.

I solve this problem without a relational database. The customer does not need to have a direct relation to a line item; the invoice does that for him/her. Similarly, the customer needs no relation to the parts catalog. With some planning, and understanding of the information problem, a series of special indices and embedded links can be constructed that give you all the data interrelations that you will need. In any setting where the interrelationships are fixed, and the data queries are predictable and have been anticipated, a combination of careful programming and good data structures are a powerful data management system.

Don’t Scare Your Customers - Think Ahead

This kind of programming requires a lot of forethought. Unlike an interface, it is hard to create data structures on the fly. It is critical to have a clear vision of the problem and how you are going to solve it, because you are hard-coding data relationships. It will be difficult for you, and frightening for your user (yes, frightening) when you twiddle an existing file to “add some stuff.” Why frightening? I’ll use myself as an example. My accounts receivable program handles billing and payments from insurance companies and from patients. I literally eat and pay my bills from the information in this file. Tess 4.4 and its programmer(s) are great.

However, what if this alert came up on the screen when I installed an upgrade? “Make sure you have three clean backup copies of your current data because we’re going to twiddle your file structure and add some stuff we forgot. We’re going to completely tear apart your file structure and rebuild it, from scratch, on the fly. Don’t do this operation during a thunderstorm. In fact, don’t even breath near the computer for the next couple of hours. This will take half an hour per megabyte of data on a Power Mac 9500. Press OK to continue.”

You and your users will need to live with your decisions for a long time. It may be worthwhile thinking more like a civil engineer in a litigious society than a software engineer trying to be elegant: overbuild, overbuild, overbuild. Talk to your end user(s). Know the problem before you try to solve it. Build in extensibility. We will see how to do that in a few paragraphs.

Invoice Example and B-Tree Helper in Detail

My example program, which is cleverly named Invoice Example, and B-Tree Helper deserve some explanation. Invoice Example is based loosely on some sample code from DTS, which I used as a framework to handle three modeless dialogs, a couple of menus, a bunch of alerts and a single modal dialog. The interface is not elegant, but it gets the job done. I have separated the code into GUI related files and function related files. The user interface code is fairly pedestrian and I have no secret techniques; the meat of this article is in the folder called “functional files.” Header files for B-Tree Helper are included, but obviously not the source files. A compiled 68K application with an example data file is available for download and on the subscription disk. The source code and the application genuinely complement this article. However, some of the important routines are long and are not well suited to hard copy presentation. Please review the source code (obtained from disk or ‘net) or many of the points may be murky.

To perform an operation, the user selects from a popup menu in the window. This sets the mode (find, next, insert, edit, etc.). When all of the data is ready, the user clicks the Do It button. In the Invoice window, two steps are required for insertion. First the user enters references to customer and parts and then clicks the Load Related button. This checks for valid data, does some math, and then loads the related records. Second, the user clicks the Do It button. Searches and deletions of invoices are simpler and require only a mode selection and a Do It click. There is a window each for Customer, Invoice, and Part operations. The fourth window is for a separate example which we will get to later.

The program reads and writes data to a file, and then uses embedded information or appropriate keys and indices to recall related information. The precise mechanism for this is in the commented sample code. We use B-Tree Helper for all file related functions with the exception of some resource calls.

B-Tree Helper consists of 40 calls that create, open and close files, insert and delete records, insert and delete keys, do complex searches, manage record buffering, add and delete index trees, and permit retrieval of raw page data for debugging. To create a file, the main programming task is the initialization of a series of index trees, which are then passed to the creation routine. These trees represent your index structures.

The other major planning task is to determine file block size (space in the file is allocated by blocks). The size of the block determines the maximum record size, because B-Tree Helper uses control records interspersed with your data to manage available file space. These control records are immovable islands in your data stream, and your records must fit between them. Thus, you must calculate your allocation block size so that your maximum sized data can fit between the control records. Although B-Tree Helper allows variable length records, you will hit a size ceiling of 8*fileBlockSize*(fileBlockSize-8) bytes unless you split your data into chunks (which is not the end of the world either). Upon successfully creating a file, a FileControlHandle - the record that manages the file while open - is returned and is used in virtually every other call to the B-Tree Helper library.

Interestingly, B-Tree Helper does not require formal definition of a record; you simply request n bytes of data from the file. If successful in allocating this block of file space, you are given a success result code and a file address. You can then write and subsequently read data from this address. This address is a four byte long and is also passed to key insertion functions. When a key is searched for and found, the key is returned and so is the file address. However, it is not required to pass a file address to the key. Any four byte value is legal, and this can be used very much to our advantage, as will be shown later. Because records are not defined, keys are also not irretrievably linked to a record type or logical file. This can also be used to our advantage. Finally, because a record is not specifically defined as to content or size, many kinds of records can coexist in a single file. Thus, I think of a B-Tree Helper file as a physical file enclosing any number of logical files, index structures, and map control records (Figure 2) B-Tree Helper is very flexible in this manner.

Figure 2.

Reduce Flexibility Creatively

When I have tackled more involved problems than Invoice Example, I have taken the time to reduce flexibility and create structures that describe each index and record type. This has saved me an enormous amount of headache when debugging and writing code.

Review of the B-Tree Helper functions shows that there is great similarity among the calls of a given function class; key related functions and record related functions all pretty much take the same kind of parameters. I have usually written parameter block structs that I then pass to a custom function that is a wrapper for the B-Tree Helper call. This relieves me of getting the parameters and degree of indirection right each time I call Insert_Key() or Find_Equal(). Additionally, I declare these parameter block structures as global or static. Just as each logical file in your database has an “active” or “current” record (i.e. a record that is loaded into memory and being operated upon), there is a corresponding active key and corresponding active file address. Although I usually separate the actual data from the parameter block, having current keys and current file addresses that can survive between function calls can be very useful, especially when doing successive operations upon multiple logical files. This is demonstrated by supporting Next and Previous functions in the example program.

I am not heavily into object oriented programming, but writing a series of wrapper classes for B-Tree Helper seems like a very sensible, productive thing for you C++ buffs out there to do. In particular, C++’s multiple inheritance would permit creating complex objects that could respond to a single command to insert a record, a key, and then link to a related record.

Making Relationships Work

Records with embedded addresses

I have repeatedly referred to linking to related records. How do we do this? I use three basic techniques.

First embed a file address into a record. Let’s examine the declaration for the CustomerType struct:

struct CustomerType{
 unsigned char   CustName[32];
 long   CustNo;
 long   StringAddr;
 };
typedef struct CustomerType CustomerType;
 unsigned char   gCustString[40];
 CustomerType  gCustomer;

I thought CustomerType had the information we need for the example; however I “forgot” to allow room for a comment. Luckily, I had thought ahead and had left an extra four byte field in CustomerType, which I then renamed StringAddr. After we retrieve all the information from the dialog, we Get_Bytes() for gCustString first, and Write_Data() from the memory address of gCustString to the file address returned by Get_Bytes(). We then set gCustomer.StringAddr to the file address returned by Get_Bytes() as well. Then, and only then, do we allocate space and write the data for the parent gCustomer record.

When information is retrieved, we follow the reverse sequence. We Read_Data() and get gCustomer first. We then Read_Data() using the file address stored in gCustomer.StringAddr to get the child gCustString.

I use this technique when the referenced record is truly a child record - it will never be looked for unto itself. This is a nice way of gracefully adding to a record which was missdeclared, and is a prime way of adding record extensibility. For this reason, I will generally add an extra unused four byte field to just about any record I declare in a “serious” program. This is one of the simplest ways of overbuilding your software to keep data integrity at a maximum. Had we used this scheme and needed to make an ex post facto change to an existing file, my update routine would have read into memory the parent record, allocated and wrote the child record (the new stuff), set the correct field of the parent record with the file address of the child record, and rewrote the parent record to file. No reindexing or reconstitution of existing data would have been necessary.

Sequence numbers to the rescue

This is a nice, simple system. It works well. Why limit its use to a strict parent/child relationship? The problem is that of editing or updating data. If gCustString were a record that had multiple links, and the file address of gCustString’s data were to change (which can easily happen when a record is edited or updated), I would need to find every record that had a link to the changed record and insert the new file address. There are two remedies to this problem. One is to create a pseudo file address that remains constant regardless of the true file address of the record, but always points to the true file address (like a handle). This strategy is used by DynaBase, a flat file database engine which is no longer commercially available. In DynaBase, all file addresses were pseudo addresses, adding overhead where none was needed.

The second approach is to use a sequence number; at allocation each record is assigned a number which is that record’s and only that record’s, never to change for the life of the file. There are a number of ways to issue sequence numbers. For the demo, I used a custom resource that is a handle to a small record whose fields are incremented whenever a record is allocated. This technique is shown in the example program and I will not dwell on it here. It is equally easy to put a sequence number dispatching record in the data fork; using B-Tree Helper to make a Get_Bytes() call for your sequence dispatching record and put the resulting file address into one of the .application1..4 fields of the FileControlHandle and the address of the record will survive across work sessions. See the Picture Creator demo for a simple example of this technique.

If we examine the declaration for the invoice struct, we see that the line item fields are in fact long integers:

struct InvoiceType {
 long   InvoiceNumber;
 long   LineItem1;
 long   LineItem2;
 long   CustNo;
 };

typedef struct InvoiceType InvoiceType;
InvoiceType gInvoice;

After each line item record has been filled in by the user, the program goes to work. A sequence number is issued to that line item record, and the appropriate field in the line item record is filled in with that sequence number. Second, that sequence number is inserted into field LineItem1 or LineItem2. Third, a key is inserted in an index tree, with the sequence number as the key pointing to the file address of the line item record.

This second way of relating a record, that is, to embed the sequence number of a record into the body of a related record, is shown in Figure 3.

Figure 3.

When an invoice is retrieved, the invoice record is retrieved first. A search is performed on the sequence number in either or both of the two line item fields in the invoice record. The file address that is retrieved is the file address of the line item to be read into memory, and a Read_Data() is performed.

This example is trivial, and each line item record belongs to one and only one invoice. Correcting the invoice record for a change in file address of the line item would be easy even if a sequence number had not been issued. However, more complex data structures may have a record that links to many other records, and each record would have to be brought into memory, fixed, and rewritten to disk. That’s a lot of error prone work. With B-Tree Helper, there is a slick function called Change_Address(), which will fix the address of a key when there is a file address change. Even if Change_Address() did not exist, this is only a few lines of code.

Indirect Index Trees

The final way to relate records is with the use of a key from one record pointing to the sequence number of a related record (Figure 4).

Figure 4.

One of the functions we have in Invoice Example is to find the invoices belonging to a customer. Using the name of the customer as the key, we insert this key pointing not to a file address, but to a long that is the sequence number for an invoice belonging to the customer - B-Tree Helper will accept any long for the “file address” portion of the key. We then use this retrieved sequence number as the key to another index where the sequence number key now points to a real file address. Thus, two key lookups must be performed for this search, but the relationship between the customer record and the invoice record will survive file address changes of the invoice nicely if just the second key is kept updated. This kind of relationship is very useful when a record may have a relation to many other relations.

For so-called “many-to-many” relationships, a series of intermediary relating records, consisting of just sequence numbers with appropriately directed keys, can fill the bill. Implementation of this solution is left as an exercise for the reader, which translated into plain English means that you may want to rethink buying one of the more complete solutions if your problem is very hairy. Just remember though, the invoice record that we created is in fact the very kind of record with pointers to other records in other files. Maybe it isn’t as hard as you thought.

General Data Management with a Database Library

The last idea on the creative use of flat file database engines has nothing to do with databases, just plain information management. Although some of this discourse may be rendered obsolete by OpenDoc, consider the problem of the complex document. Several years ago I wrote a program that did extensive data crunching of information on patients’ sleep. There were half a dozen header records of fixed length, and 7 arrays that could vary anywhere between a couple of hundred and several thousand bytes a piece, depending on the severity of the sleep disturbance. Rather than reinvent the wheel to save this complex document to disk, I simply used a database manager. I viewed each piece of information as a variable length record within a logical file. I let the database do all the insertions, deletions, and updates for me, rather than writing routines from scratch. With the arrays varying so greatly in size (I saved the arrays as a single block, rather than element by element), it was much easier to use generic pre-built routines that handled variable length records.

As an example of this solution, I have made a fourth dialog window that opens a picture of Joshua Chamberlain (one of our most famous Mainers) and a short text blurb about him. While this could have been saved as a couple of resources (and in fact the example file was created from resources), the picture and text are in the data fork of a B-Tree Helper file. This is one of the most intriguing uses of a database engine, particularly for the independent, small project programmer.

My last point to make has to do with error checking. Do it. Do it a lot. My example doesn’t do it half enough. The only specific advice I have is to insert data first and keys second. It is much worse to have a key pointing to nowhere than an orphan record, and it makes the cleanup procedure after a failed read/write much easier.

Punch Line

To summarize, here are the salient points of this article:

• Not every database problem needs a relational database.

• Use a flexible flat file database manager as your major tool for data management.

• Consider and construct your data relations very carefully.

• Add an extra four byte field or two to your record declarations. They may come in handy later.

• Consider writing custom functions around the database toolbox calls to improve readability, reduce errors during programming, and make file management easier. If working in an object oriented environment, consider building a series of good quality wrapper classes for the function/parameter block entities that can be reused.

• For simple parent-child data relations, simply embed the address of the child in the parent record.

• Use embedded sequence numbers for relatively simple relations where the child record may be moved within the file.

• Use keys from one record pointing the sequence number of another record to create complex data relations.

• Consider using database engines to manage complex documents.

• Error check till you can’t stand the sight of an if (myErr != noErr) clause.

May you all be spared the ignominy of my first project.

 

Community Search:
MacTech Search:

Software Updates via MacUpdate

Dropbox 193.4.5594 - Cloud backup and sy...
Dropbox is a file hosting service that provides cloud storage, file synchronization, personal cloud, and client software. It is a modern workspace that allows you to get to all of your files, manage... Read more
Google Chrome 122.0.6261.57 - Modern and...
Google Chrome is a Web browser by Google, created to be a modern platform for Web pages and applications. It utilizes very fast loading of Web pages and has a V8 engine, which is a custom built... Read more
Skype 8.113.0.210 - Voice-over-internet...
Skype is a telecommunications app that provides HD video calls, instant messaging, calling to any phone number or landline, and Skype for Business for productive cooperation on the projects. This... Read more
Tor Browser 13.0.10 - Anonymize Web brow...
Using Tor Browser you can protect yourself against tracking, surveillance, and censorship. Tor was originally designed, implemented, and deployed as a third-generation onion-routing project of the U.... Read more
Deeper 3.0.4 - Enable hidden features in...
Deeper is a personalization utility for macOS which allows you to enable and disable the hidden functions of the Finder, Dock, QuickTime, Safari, iTunes, login window, Spotlight, and many of Apple's... Read more
OnyX 4.5.5 - Maintenance and optimizatio...
OnyX is a multifunction utility that you can use to verify the startup disk and the structure of its system files, to run miscellaneous maintenance and cleaning tasks, to configure parameters in the... Read more

Latest Forum Discussions

See All

Zenless Zone Zero opens entries for its...
miHoYo, aka HoYoverse, has become such a big name in mobile gaming that it's hard to believe that arguably their flagship title, Genshin Impact, is only three and a half years old. Now, they continue the road to the next title in their world, with... | Read more »
Live, Playdate, Live! – The TouchArcade...
In this week’s episode of The TouchArcade Show we kick things off by talking about all the games I splurged on during the recent Playdate Catalog one-year anniversary sale, including the new Lucas Pope jam Mars After Midnight. We haven’t played any... | Read more »
TouchArcade Game of the Week: ‘Vroomies’
So here’s a thing: Vroomies from developer Alex Taber aka Unordered Games is the Game of the Week! Except… Vroomies came out an entire month ago. It wasn’t on my radar until this week, which is why I included it in our weekly new games round-up, but... | Read more »
SwitchArcade Round-Up: ‘MLB The Show 24’...
Hello gentle readers, and welcome to the SwitchArcade Round-Up for March 15th, 2024. We’re closing out the week with a bunch of new games, with Sony’s baseball franchise MLB The Show up to bat yet again. There are several other interesting games to... | Read more »
Steam Deck Weekly: WWE 2K24 and Summerho...
Welcome to this week’s edition of the Steam Deck Weekly. The busy season has begun with games we’ve been looking forward to playing including Dragon’s Dogma 2, Horizon Forbidden West Complete Edition, and also console exclusives like Rise of the... | Read more »
Steam Spring Sale 2024 – The 10 Best Ste...
The Steam Spring Sale 2024 began last night, and while it isn’t as big of a deal as say the Steam Winter Sale, you may as well take advantage of it to save money on some games you were planning to buy. I obviously recommend checking out your own... | Read more »
New ‘SaGa Emerald Beyond’ Gameplay Showc...
Last month, Square Enix posted a Let’s Play video featuring SaGa Localization Director Neil Broadley who showcased the worlds, companions, and more from the upcoming and highly-anticipated RPG SaGa Emerald Beyond. | Read more »
Choose Your Side in the Latest ‘Marvel S...
Last month, Marvel Snap (Free) held its very first “imbalance" event in honor of Valentine’s Day. For a limited time, certain well-known couples were given special boosts when conditions were right. It must have gone over well, because we’ve got a... | Read more »
Warframe welcomes the arrival of a new s...
As a Warframe player one of the best things about it launching on iOS, despite it being arguably the best way to play the game if you have a controller, is that I can now be paid to talk about it. To whit, we are gearing up to receive the first... | Read more »
Apple Arcade Weekly Round-Up: Updates an...
Following the new releases earlier in the month and April 2024’s games being revealed by Apple, this week has seen some notable game updates and events go live for Apple Arcade. What The Golf? has an April Fool’s Day celebration event going live “... | Read more »

Price Scanner via MacPrices.net

Apple Education is offering $100 discounts on...
If you’re a student, teacher, or staff member at any educational institution, you can use your .edu email address when ordering at Apple Education to take $100 off the price of a new M3 MacBook Air.... Read more
Apple Watch Ultra 2 with Blood Oxygen feature...
Best Buy is offering Apple Watch Ultra 2 models for $50 off MSRP on their online store this week. Sale prices available for online orders only, in-store prices may vary. Order online, and choose... Read more
New promo at Sams Club: Apple HomePods for $2...
Sams Club has Apple HomePods on sale for $259 through March 31, 2024. Their price is $40 off Apple’s MSRP, and both Space Gray and White colors are available. Sale price is for online orders only, in... Read more
Get Apple’s 2nd generation Apple Pencil for $...
Apple’s Pencil (2nd generation) works with the 12″ iPad Pro (3rd, 4th, 5th, and 6th generation), 11″ iPad Pro (1st, 2nd, 3rd, and 4th generation), iPad Air (4th and 5th generation), and iPad mini (... Read more
10th generation Apple iPads on sale for $100...
Best Buy has Apple’s 10th-generation WiFi iPads back on sale for $100 off MSRP on their online store, starting at only $349. With the discount, Best Buy’s prices are the lowest currently available... Read more
iPad Airs on sale again starting at $449 on B...
Best Buy has 10.9″ M1 WiFi iPad Airs on record-low sale prices again for $150 off Apple’s MSRP, starting at $449. Sale prices for online orders only, in-store price may vary. Order online, and choose... Read more
Best Buy is blowing out clearance 13-inch M1...
Best Buy is blowing out clearance Apple 13″ M1 MacBook Airs this weekend for only $649.99, or $350 off Apple’s original MSRP. Sale prices for online orders only, in-store prices may vary. Order... Read more
Low price alert! You can now get a 13-inch M1...
Walmart has, for the first time, begun offering new Apple MacBooks for sale on their online store, albeit clearance previous-generation models. They now have the 13″ M1 MacBook Air (8GB RAM, 256GB... Read more
Best Apple MacBook deal this weekend: Get the...
Apple has 13″ M2 MacBook Airs available for only $849 today in their Certified Refurbished store. These are the cheapest M2-powered MacBooks for sale at Apple. Apple’s one-year warranty is included,... Read more
New 15-inch M3 MacBook Air (Midnight) on sale...
Amazon has the new 15″ M3 MacBook Air (8GB RAM/256GB SSD/Midnight) in stock and on sale today for $1249.99 including free shipping. Their price is $50 off MSRP, and it’s the lowest price currently... Read more

Jobs Board

Early Preschool Teacher - Glenda Drive/ *Appl...
Early Preschool Teacher - Glenda Drive/ Apple ValleyTeacher Share by Email Share on LinkedIn Share on Twitter Read more
Senior Software Engineer - *Apple* Fundamen...
…center of Microsoft's efforts to empower our users to do more. The Apple Fundamentals team focused on defining and improving the end-to-end developer experience in Read more
Relationship Banker *Apple* Valley Main - W...
…Alcohol Policy to learn more. **Company:** WELLS FARGO BANK **Req Number:** R-350696 **Updated:** Mon Mar 11 00:00:00 UTC 2024 **Location:** APPLE VALLEY,California Read more
Medical Assistant - Surgical Oncology- *Apple...
Medical Assistant - Surgical Oncology- Apple Hill WellSpan Medical Group, York, PA | Nursing | Nursing Support | FTE: 1 | Regular | Tracking Code: 200555 Apply Now Read more
Early Preschool Teacher - Glenda Drive/ *Appl...
Early Preschool Teacher - Glenda Drive/ Apple ValleyTeacher Share by Email Share on LinkedIn Share on Twitter Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.