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 ( 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

The best GIF making apps
Animated GIFs have exploded in popularity recently which is likely thanks to a combination of Tumblr, our shorter attention spans, and the simple fact they’re a lot of fun. [Read more] | Read more »
The best remote desktop apps for iOS
We've been sifting through the App Store to find the best ways to do computer tasks on a tablet. That gave us a thought - what if we could just do computer tasks from our tablets? Here's a list of the best remote desktop apps to help you use your... | Read more »
Warhammer 40,000: Freeblade guide - How...
Warhammer 40,000: Freebladejust launched in the App Store and it lets you live your childhood dream of blowing up and slashing a bunch of enemies as a massive, hulking Space Marine. It's not easy being a Space Marine though - and particularly if... | Read more »
Gopogo guide - How to bounce like the be...
Nitrome just launched a new game and, as to be expected, it's a lot of addictive fun. It's called Gopogo, and it challenges you to hoparound a bunch of platforms, avoiding enemies and picking up shiny stuff. It's not easy though - just like the... | Read more »
Sago Mini Superhero (Education)
Sago Mini Superhero 1.0 Device: iOS Universal Category: Education Price: $2.99, Version: 1.0 (iTunes) Description: KAPOW! Jack the rabbit bursts into the sky as the Sago Mini Superhero! Fly with Jack as he lifts impossible weights,... | Read more »
Star Wars: Galaxy of Heroes guide - How...
Star Wars: Galaxy of Heroes is all about collecting heroes, powering them up, and using them together to defeat your foes. It's pretty straightforward stuff for the most part, but increasing your characters' stats can be a bit confusing because it... | Read more »
The best cooking apps (just in time for...
It’s that time of year again, where you’ll be gathering around the dinner table with your family and a huge feast in front of you. [Read more] | Read more »
Square Rave guide - How to grab those te...
Square Rave is an awesome little music-oriented puzzle game that smacks of games like Lumines, but with its own unique sense of gameplay. To help wrap your head around the game, keep the following tips and tricks in mind. [Read more] | Read more »
Snowboard Party 2 (Games)
Snowboard Party 2 1.0 Device: iOS Universal Category: Games Price: $1.99, Version: 1.0 (iTunes) Description: Crowned the best snowboarding game available on the market, Snowboard Party is back to fulfill all your adrenaline needs in... | Read more »
One Button Travel (Games)
One Button Travel 1.0 Device: iOS Universal Category: Games Price: $2.99, Version: 1.0 (iTunes) Description: “To cut a long story short, If you like interactive fiction, just go buy this one.” - “Oozes the polish that... | Read more »

Price Scanner via

Holiday weekend Mac sales roundup: B&H Ph...
B&H Photo continues to have all new Macs on sale for up to $500 off MSRP as part of their Black Friday/Holiday weekend sale. Shipping is free, and B&H charges NY tax only: - 15″ 2.2GHz Retina... Read more
iMobie Releases its Ace iOS Cleaner PhoneClea...
iMobie Inc. has announced the new update of PhoneClean 4, its iOS cleaner designed to reclaim wasted space on iPhone/iPad for use and keep the device fast. Alongside, iMobie hosts a 3-day giveaway of... Read more
U.S. Cellular Offering iPad Pro
U.S. Cellular today announced that it is offering the new iPad Pro with Wi-Fi + Cellular, featuring a 12.9-inch Retina display with 5.6 million pixels — the most ever in an iOS device. U.S. Cellular... Read more
Newegg Canada Unveils Black Friday Deals for...
Newegg Canada is offering more than 1,000 deep discounts to Canadian customers this Black Friday, available now through Cyber Monday, with new deals posted throughout the week. “Black Friday is... Read more
Black Friday: Macs on sale for up to $500 off...
BLACK FRIDAY B&H Photo has all new Macs on sale for up to $500 off MSRP as part of their early Black Friday sale including free shipping plus NY sales tax only: - 15″ 2.2GHz Retina MacBook Pro: $... Read more
Black Friday: Up to $125 off iPad Air 2s at B...
BLACK FRIDAY Walmart has the 16GB iPad Air 2 WiFi on sale for $100 off MSRP on their online store. Choose free shipping or free local store pickup (if available): - 16GB iPad Air 2 WiFi: $399, save $... Read more
Black Friday: iPad mini 4s on sale for $100 o...
BLACK FRIDAY Best Buy has iPad mini 4s on sale for $100 off MSRP on their online store for Black Friday. Choose free shipping or free local store pickup (if available): - 16GB iPad mini 4 WiFi: $299.... Read more
Black Friday: Apple Watch for up to $100 off...
BLACK FRIDAY Apple resellers are offering discounts and bundles with the purchase of an Apple Watch this Black Friday. Below is a roundup of the deals being offered by authorized Watch resellers:... Read more
Black Friday: Target offers 6th Generation iP...
BLACK FRIDAY Save $40 to $60 on a 6th generation iPod touch at Target with free shipping or free local store pickup (if available). Sale prices for online orders only, in-store prices may vary: -... Read more
Black Friday: Walmart and Target offer iPod n...
BLACK FRIDAY Walmart has the 16GB iPod nano (various colors) on sale for $119.20 on their online store for a limited time. That’s $30 off MSRP. Choose free shipping or free local store pickup (if... Read more

Jobs Board

Specialist *Apple* /Mac Desktop - University...
…technical support, expertise and user training for a variety of Apple /Macintosh hardware, software and devices.Researches, analyzes and resolves complex Apple Read more
*Apple* Site Security Manager - Apple (Unite...
# Apple Site Security Manager Job Number: 42975010 Culver City, Califo ia, United States Posted: Oct. 2, 2015 Weekly Hours: 40.00 **Job Summary** The Apple Site Read more
WiSE *Apple* Pay Quality Engineer - Apple (...
# WiSE Apple Pay Quality Engineer Job Number: 44313381 Santa Clara Valley, Califo ia, United States Posted: Nov. 13, 2015 Weekly Hours: 40.00 **Job Summary** Join our Read more
Holiday Retail Associate with *Apple* Knowl...
…and assertive.Someone who can troubleshoot iOS devices (iPhone and iPad) and Apple Mail issues.Someone who can offer solutions.Someone who can work weekends.Someone with Read more
*Apple* Systems Engineer (Mclean, VA and NYC...
Summary:Assist in providing strategic direction and technical leadership within the Apple portfolio, including desktops, laptops, and printing environment. This person Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.